mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 08:48:13 +00:00
Add ability to generate thumbnails
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -16,7 +15,7 @@ from werkzeug.utils import secure_filename
|
|||||||
|
|
||||||
from lib.render_job import RenderJob
|
from lib.render_job import RenderJob
|
||||||
from lib.render_queue import RenderQueue
|
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
|
from utilities.render_worker import RenderWorkerFactory, string_to_status, RenderStatus
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
@@ -64,6 +63,25 @@ def job_detail(job_id):
|
|||||||
return f'Cannot find job with ID {job_id}', 400
|
return f'Cannot find job with ID {job_id}', 400
|
||||||
|
|
||||||
|
|
||||||
|
@server.route('/ui/job/<job_id>/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
|
# Get job file routing
|
||||||
@server.route('/api/job/<job_id>/file/<filename>', methods=['GET'])
|
@server.route('/api/job/<job_id>/file/<filename>', methods=['GET'])
|
||||||
def get_job_file(job_id, filename):
|
def get_job_file(job_id, filename):
|
||||||
@@ -125,7 +143,6 @@ def get_file_list(job_id):
|
|||||||
|
|
||||||
@server.route('/api/job/<job_id>/download_all')
|
@server.route('/api/job/<job_id>/download_all')
|
||||||
def download_all(job_id):
|
def download_all(job_id):
|
||||||
|
|
||||||
zip_filename = None
|
zip_filename = None
|
||||||
|
|
||||||
@after_this_request
|
@after_this_request
|
||||||
@@ -206,7 +223,6 @@ def snapshot():
|
|||||||
|
|
||||||
@server.post('/api/add_job')
|
@server.post('/api/add_job')
|
||||||
def add_job_handler():
|
def add_job_handler():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
"""Create new job and add to server render queue"""
|
"""Create new job and add to server render queue"""
|
||||||
if request.is_json:
|
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):
|
if server.config['UPLOAD_FOLDER'] in output_dir and os.path.exists(output_dir):
|
||||||
shutil.rmtree(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)
|
# See if we own the input file (i.e. was it uploaded)
|
||||||
input_dir = os.path.dirname(found_job.worker.input_path)
|
input_dir = os.path.dirname(found_job.worker.input_path)
|
||||||
if server.config['UPLOAD_FOLDER'] in input_dir and os.path.exists(input_dir):
|
if server.config['UPLOAD_FOLDER'] in input_dir and os.path.exists(input_dir):
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import subprocess
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import json
|
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):
|
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)
|
req = requests.post(f'http://{client}:{server_port}/api/add_job', files=job_files)
|
||||||
return req
|
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()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ def start_server(background_thread=False):
|
|||||||
level=config.get('server_log_level', 'INFO').upper())
|
level=config.get('server_log_level', 'INFO').upper())
|
||||||
|
|
||||||
server.config['UPLOAD_FOLDER'] = os.path.expanduser(config['upload_folder'])
|
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']
|
server.config['MAX_CONTENT_PATH'] = config['max_content_path']
|
||||||
|
|
||||||
# Get hostname and render clients
|
# Get hostname and render clients
|
||||||
|
|||||||
@@ -37,20 +37,20 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth is-narrow">
|
<table class="table is-bordered is-striped is-hoverable is-fullwidth is-narrow" style="text-align: center; vertical-align: middle;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Preview</th>
|
<th style="text-align: center;">Preview</th>
|
||||||
<th>Name</th>
|
<th style="text-align: center;">Name</th>
|
||||||
<th>Renderer</th>
|
<th style="text-align: center;">Renderer</th>
|
||||||
<th>Priority</th>
|
<th style="text-align: center;">Priority</th>
|
||||||
<th>Status</th>
|
<th style="text-align: center;">Status</th>
|
||||||
<th>Time Elapsed</th>
|
<th style="text-align: center;">Time Elapsed</th>
|
||||||
<th>%</th>
|
<th style="text-align: center;">%</th>
|
||||||
<th>Frame Count</th>
|
<th style="text-align: center;">Frame Count</th>
|
||||||
<th>Client</th>
|
<th style="text-align: center;">Client</th>
|
||||||
<th>Last Output</th>
|
<th style="text-align: center;">Last Output</th>
|
||||||
<th>Commands</th>
|
<th style="text-align: center;">Commands</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
{% for job in all_jobs %}
|
{% for job in all_jobs %}
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Image Here</td>
|
<td style="padding: 0; margin: 0;"><img src="/ui/job/{{job.id}}/thumbnail"></td>
|
||||||
<td>{{job.name}}</td>
|
<td>{{job.name}}</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>
|
||||||
|
|||||||
@@ -9,9 +9,16 @@ def file_info(path):
|
|||||||
return None
|
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):
|
def generate_fast_preview(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': '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)
|
return _run_output(stream, run_async)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user