diff --git a/lib/job_server.py b/lib/job_server.py index e048b4d..b6f8d27 100755 --- a/lib/job_server.py +++ b/lib/job_server.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import json import logging import os @@ -16,7 +15,7 @@ from werkzeug.utils import secure_filename from lib.render_job import RenderJob from lib.render_queue import RenderQueue -from lib.server_helper import post_job_to_server +from lib.server_helper import post_job_to_server, generate_thumbnail_for_job from utilities.render_worker import RenderWorkerFactory, string_to_status, RenderStatus logger = logging.getLogger() @@ -64,6 +63,25 @@ def job_detail(job_id): return f'Cannot find job with ID {job_id}', 400 +@server.route('/ui/job//thumbnail') +def job_thumbnail(job_id): + found_job = RenderQueue.job_with_id(job_id) + if found_job: + + os.makedirs(server.config['THUMBS_FOLDER'], exist_ok=True) + thumb_video_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.mp4') + 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'): + generate_thumbnail_for_job(found_job, thumb_video_path, thumb_image_path, max_width=120) + + 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") + elif os.path.exists(thumb_image_path): + return send_file(thumb_image_path, mimetype='image/jpeg') + return send_file('static/spinner.gif', mimetype="image/gif") + + # Get job file routing @server.route('/api/job//file/', methods=['GET']) def get_job_file(job_id, filename): @@ -125,7 +143,6 @@ def get_file_list(job_id): @server.route('/api/job//download_all') def download_all(job_id): - zip_filename = None @after_this_request @@ -206,7 +223,6 @@ def snapshot(): @server.post('/api/add_job') def add_job_handler(): - try: """Create new job and add to server render queue""" if request.is_json: @@ -381,6 +397,15 @@ def delete_job(job_id): if server.config['UPLOAD_FOLDER'] in output_dir and os.path.exists(output_dir): shutil.rmtree(output_dir) + # Remove any thumbnails + for filename in os.listdir(server.config['THUMBS_FOLDER']): + if job_id in filename: + os.remove(os.path.join(server.config['THUMBS_FOLDER'], filename)) + + thumb_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.mp4') + if os.path.exists(thumb_path): + os.remove(thumb_path) + # See if we own the input file (i.e. was it uploaded) input_dir = os.path.dirname(found_job.worker.input_path) if server.config['UPLOAD_FOLDER'] in input_dir and os.path.exists(input_dir): diff --git a/lib/server_helper.py b/lib/server_helper.py index 869ade9..21d4856 100644 --- a/lib/server_helper.py +++ b/lib/server_helper.py @@ -1,6 +1,10 @@ +import subprocess import requests import os import json +import threading +from utilities.render_worker import RenderStatus +from utilities.ffmpeg_presets import generate_fast_preview, save_first_frame def post_job_to_server(input_path, job_list, client, server_port=8080): @@ -10,3 +14,29 @@ def post_job_to_server(input_path, job_list, client, server_port=8080): req = requests.post(f'http://{client}:{server_port}/api/add_job', files=job_files) return req + + +def generate_thumbnail_for_job(job, thumb_video_path, thumb_image_path, max_width=320): + + # Simple thread to generate thumbs in background + def generate_thumb_thread(source): + in_progress_path = thumb_video_path + '_IN-PROGRESS' + subprocess.run(['touch', in_progress_path]) + generate_fast_preview(source_path=source, dest_path=thumb_video_path, max_width=max_width) + os.remove(in_progress_path) + + # Determine best source file to use for thumbs + if job.render_status() == RenderStatus.COMPLETED: # use finished file for thumb + source_path = job.file_list() + elif len(job.file_list()) > 1: # if image sequence, use second to last file (last may be in use) + source_path = [job.file_list()[-2]] + else: + source_path = [job.worker.input_path] # use source if nothing else + + # Todo: convert image sequence to animated movie + valid_formats = ['.mp4', '.mov', '.avi', '.mpg', '.mpeg', '.jpg', '.png', '.exr', '.mxf'] + is_valid_file_type = any(ele in source_path[0] for ele in valid_formats) + if is_valid_file_type and not os.path.exists(thumb_video_path): + save_first_frame(source_path=source_path[0], dest_path=thumb_image_path, max_width=max_width) + x = threading.Thread(target=generate_thumb_thread, args=(source_path[0],)) + x.start() diff --git a/start_server.py b/start_server.py index 40cfd05..16aff08 100755 --- a/start_server.py +++ b/start_server.py @@ -22,6 +22,7 @@ def start_server(background_thread=False): level=config.get('server_log_level', 'INFO').upper()) server.config['UPLOAD_FOLDER'] = os.path.expanduser(config['upload_folder']) + server.config['THUMBS_FOLDER'] = os.path.join(os.path.expanduser(config['upload_folder']), 'thumbs') server.config['MAX_CONTENT_PATH'] = config['max_content_path'] # Get hostname and render clients diff --git a/templates/index.html b/templates/index.html index d52a5ee..0239838 100644 --- a/templates/index.html +++ b/templates/index.html @@ -37,20 +37,20 @@
- +
- - - - - - - - - - - + + + + + + + + + + + @@ -61,7 +61,7 @@ {% for job in all_jobs %} - + diff --git a/utilities/ffmpeg_presets.py b/utilities/ffmpeg_presets.py index 9e8f36a..acbbf2b 100644 --- a/utilities/ffmpeg_presets.py +++ b/utilities/ffmpeg_presets.py @@ -9,9 +9,16 @@ def file_info(path): return None +def save_first_frame(source_path, dest_path, max_width=1280, run_async=False): + stream = ffmpeg.input(source_path) + stream = ffmpeg.output(stream, dest_path, **{'vf': f'format=yuv420p,scale={max_width}:-2', 'vframes': '1'}) + return _run_output(stream, run_async) + + def generate_fast_preview(source_path, dest_path, max_width=1280, run_async=False): stream = ffmpeg.input(source_path) - stream = ffmpeg.output(stream, dest_path, **{'vf': 'format=yuv420p,scale={width}:-2'.format(width=max_width), 'preset': 'ultrafast'}) + stream = ffmpeg.output(stream, dest_path, **{'vf': 'format=yuv420p,scale={width}:-2'.format(width=max_width), + 'preset': 'ultrafast'}) return _run_output(stream, run_async)
PreviewNameRendererPriorityStatusTime Elapsed%Frame CountClientLast OutputCommandsPreviewNameRendererPriorityStatusTime Elapsed%Frame CountClientLast OutputCommands
Image Here {{job.name}} {{job.renderer}}-{{job.worker.renderer_version}} {{job.priority}}