Improvements to thumbnail generation and detail page
@@ -55,12 +55,13 @@ def job_detail(job_id):
|
|||||||
found_job = RenderQueue.job_with_id(job_id)
|
found_job = RenderQueue.job_with_id(job_id)
|
||||||
if found_job:
|
if found_job:
|
||||||
table_html = json2html.json2html.convert(json=found_job.json(),
|
table_html = json2html.json2html.convert(json=found_job.json(),
|
||||||
table_attributes='class="table is-narrow is-striped"')
|
table_attributes='class="table is-narrow is-striped is-fullwidth"')
|
||||||
media_url = None
|
media_url = None
|
||||||
if found_job.file_list():
|
if found_job.file_list() and found_job.render_status() == RenderStatus.COMPLETED:
|
||||||
media_basename = os.path.basename(found_job.file_list()[0])
|
media_basename = os.path.basename(found_job.file_list()[0])
|
||||||
media_url = f"/api/job/{job_id}/file/{media_basename}"
|
media_url = f"/api/job/{job_id}/file/{media_basename}"
|
||||||
return render_template('details.html', detail_table=table_html, media_url=media_url)
|
return render_template('details.html', detail_table=table_html, media_url=media_url,
|
||||||
|
job_status=found_job.render_status().value.title(), job=found_job)
|
||||||
return f'Cannot find job with ID {job_id}', 400
|
return f'Cannot find job with ID {job_id}', 400
|
||||||
|
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ def job_thumbnail(job_id):
|
|||||||
thumb_image_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.jpg')
|
thumb_image_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.jpg')
|
||||||
|
|
||||||
if not os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS'):
|
if not os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS'):
|
||||||
generate_thumbnail_for_job(found_job, thumb_video_path, thumb_image_path, max_width=120)
|
generate_thumbnail_for_job(found_job, thumb_video_path, thumb_image_path, max_width=240)
|
||||||
|
|
||||||
if os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS'):
|
if os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS'):
|
||||||
return send_file(thumb_video_path, mimetype="video/mp4")
|
return send_file(thumb_video_path, mimetype="video/mp4")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
from utilities.render_worker import RenderStatus
|
from utilities.render_worker import RenderStatus
|
||||||
from utilities.ffmpeg_presets import generate_fast_preview, save_first_frame
|
from utilities.ffmpeg_presets import generate_thumbnail, save_first_frame
|
||||||
|
|
||||||
|
|
||||||
def post_job_to_server(input_path, job_list, client, server_port=8080):
|
def post_job_to_server(input_path, job_list, client, server_port=8080):
|
||||||
@@ -22,7 +22,7 @@ def generate_thumbnail_for_job(job, thumb_video_path, thumb_image_path, max_widt
|
|||||||
def generate_thumb_thread(source):
|
def generate_thumb_thread(source):
|
||||||
in_progress_path = thumb_video_path + '_IN-PROGRESS'
|
in_progress_path = thumb_video_path + '_IN-PROGRESS'
|
||||||
subprocess.run(['touch', in_progress_path])
|
subprocess.run(['touch', in_progress_path])
|
||||||
generate_fast_preview(source_path=source, dest_path=thumb_video_path, max_width=max_width)
|
generate_thumbnail(source_path=source, dest_path=thumb_video_path, max_width=max_width)
|
||||||
os.remove(in_progress_path)
|
os.remove(in_progress_path)
|
||||||
|
|
||||||
# Determine best source file to use for thumbs
|
# Determine best source file to use for thumbs
|
||||||
|
|||||||
BIN
lib/static/cancelled.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
lib/static/error.png
Normal file
|
After Width: | Height: | Size: 995 B |
BIN
lib/static/gears.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
lib/static/not_started.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
lib/static/scheduled.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 66 KiB |
@@ -24,8 +24,28 @@
|
|||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container table-container">
|
<div class="container table-container" style="text-align:center; width: 100%">
|
||||||
{{detail_table|safe}}
|
{% if media_url: %}
|
||||||
|
<video width="1280" height="720" controls>
|
||||||
|
<source src="{{media_url}}" type="video/mp4">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
{% elif job_status == 'Running': %}
|
||||||
|
<div style="width: 100%; height: 720px; position: relative; background: black;">
|
||||||
|
<img src="/static/gears.png" style="vertical-align: middle; width: auto; height: auto; position:absolute; margin: auto; top: 0;bottom: 0; left: 0; right: 0;">
|
||||||
|
<span style="height: auto; position:absolute; margin: auto; top: 60%; left: 0; right: 0; color: white;">
|
||||||
|
Rendering - {{ '{0:0.0f}'.format(job.percent_complete() * 100) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="width: 100%; height: 720px; position: relative; background: black;">
|
||||||
|
<img src="/static/{{job_status}}.png" style="vertical-align: middle; width: auto; height: auto; position:absolute; margin: auto; top: 0;bottom: 0; left: 0; right: 0;">
|
||||||
|
<span style="height: auto; position:absolute; margin: auto; top: 60%; left: 0; right: 0; color: white;">
|
||||||
|
{{job_status}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{{detail_table|safe}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -61,8 +61,12 @@
|
|||||||
{% for job in all_jobs %}
|
{% for job in all_jobs %}
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 0; margin: 0;"><img src="/ui/job/{{job.id}}/thumbnail"></td>
|
<td style="padding: 0; margin: 0;">
|
||||||
<td>{{job.name}}</td>
|
<a href="/ui/job/{{job.id}}/full_details">
|
||||||
|
<img src="/ui/job/{{job.id}}/thumbnail" width="300">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><a href="/ui/job/{{job.id}}/full_details">{{job.name}}</a></td>
|
||||||
<td>{{job.renderer}}-{{job.worker.renderer_version}}</td>
|
<td>{{job.renderer}}-{{job.worker.renderer_version}}</td>
|
||||||
<td>{{job.priority}}</td>
|
<td>{{job.priority}}</td>
|
||||||
<td>{{job.render_status().value}}</td>
|
<td>{{job.render_status().value}}</td>
|
||||||
@@ -70,7 +74,7 @@
|
|||||||
<td>{{ '{0:0.0f}'.format(job.percent_complete() * 100) }}%</td>
|
<td>{{ '{0:0.0f}'.format(job.percent_complete() * 100) }}%</td>
|
||||||
<td>{{job.frame_count()}}</td>
|
<td>{{job.frame_count()}}</td>
|
||||||
<td>{{job.client}}</td>
|
<td>{{job.client}}</td>
|
||||||
<td>{{job.worker.last_output}}</td>
|
<td><a href="/api/job/{{job.id}}/logs">{{job.worker.last_output}}</a></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="buttons are-small">
|
<div class="buttons are-small">
|
||||||
<button class="button is-info" onclick="window.location.href='/ui/job/{{job.id}}/full_details';">
|
<button class="button is-info" onclick="window.location.href='/ui/job/{{job.id}}/full_details';">
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ def file_info(path):
|
|||||||
|
|
||||||
def save_first_frame(source_path, dest_path, max_width=1280, run_async=False):
|
def save_first_frame(source_path, dest_path, max_width=1280, run_async=False):
|
||||||
stream = ffmpeg.input(source_path)
|
stream = ffmpeg.input(source_path)
|
||||||
stream = ffmpeg.output(stream, dest_path, **{'vf': f'format=yuv420p,scale={max_width}:-2', 'vframes': '1'})
|
stream = ffmpeg.output(stream, dest_path, **{'vf': f'format=yuv420p,scale={max_width}:trunc(ow/a/2)*2',
|
||||||
|
'vframes': '1'})
|
||||||
return _run_output(stream, run_async)
|
return _run_output(stream, run_async)
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +23,16 @@ def generate_fast_preview(source_path, dest_path, max_width=1280, run_async=Fals
|
|||||||
return _run_output(stream, run_async)
|
return _run_output(stream, run_async)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_thumbnail(source_path, dest_path, max_width=240, run_async=False):
|
||||||
|
stream = ffmpeg.input(source_path).video
|
||||||
|
stream = ffmpeg.output(stream, dest_path, **{'vf': f'scale={max_width}:trunc(ow/a/2)*2',
|
||||||
|
'preset': 'veryfast',
|
||||||
|
'r': '15',
|
||||||
|
'c:v': 'libx265',
|
||||||
|
'tag:v': 'hvc1'})
|
||||||
|
return _run_output(stream, run_async)
|
||||||
|
|
||||||
|
|
||||||
def generate_prores_trim(source_path, dest_path, start_frame, end_frame, handles=10, run_async=False):
|
def generate_prores_trim(source_path, dest_path, start_frame, end_frame, handles=10, run_async=False):
|
||||||
stream = ffmpeg.input(source_path)
|
stream = ffmpeg.input(source_path)
|
||||||
stream = stream.trim(**{'start_frame': max(start_frame-handles, 0), 'end_frame': end_frame + handles})
|
stream = stream.trim(**{'start_frame': max(start_frame-handles, 0), 'end_frame': end_frame + handles})
|
||||||
@@ -31,9 +42,10 @@ def generate_prores_trim(source_path, dest_path, start_frame, end_frame, handles
|
|||||||
|
|
||||||
|
|
||||||
def _run_output(stream, run_async):
|
def _run_output(stream, run_async):
|
||||||
return ffmpeg.run_async(stream) if run_async else ffmpeg.run(stream)
|
return ffmpeg.run_async(stream, quiet=True, overwrite_output=True) if run_async else \
|
||||||
|
ffmpeg.run(stream, quiet=True, overwrite_output=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
x = file_info("/Users/brettwilliams/Desktop/dark_knight_rises.mp4")
|
x = generate_thumbnail("/Users/brett/Desktop/pexels.mp4", "/Users/brett/Desktop/test-output.mp4", max_width=320)
|
||||||
print(x)
|
print(x)
|
||||||
|
|||||||