Blender image sequences now generate a preview mp4 on completion

This commit is contained in:
Brett Williams
2023-05-30 21:59:04 -05:00
parent 22cfe9c24e
commit 60eeb6d5e2
9 changed files with 64 additions and 34 deletions

View File

@@ -7,8 +7,7 @@ import requests
from sqlalchemy import create_engine, Column, String, Integer from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from .render_workers.base_worker import RenderStatus from .render_workers.base_worker import RenderStatus, BaseRenderWorker, Base
from .scheduled_job import ScheduledJob, Base
logger = logging.getLogger() logger = logging.getLogger()
@@ -45,7 +44,6 @@ class RenderQueue:
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
session = Session() session = Session()
ScheduledJob.register_user_events()
job_queue = [] job_queue = []
maximum_renderer_instances = {'blender': 1, 'aerender': 1, 'ffmpeg': 4} maximum_renderer_instances = {'blender': 1, 'aerender': 1, 'ffmpeg': 4}
hostname = None hostname = None
@@ -62,7 +60,7 @@ class RenderQueue:
def add_to_render_queue(cls, render_job, force_start=False, client=None): def add_to_render_queue(cls, render_job, force_start=False, client=None):
if not client or render_job.client == cls.hostname: if not client or render_job.client == cls.hostname:
logger.debug('Adding priority {} job to render queue: {}'.format(render_job.priority, render_job.worker_object)) logger.debug('Adding priority {} job to render queue: {}'.format(render_job.priority, render_job))
render_job.client = cls.hostname render_job.client = cls.hostname
cls.job_queue.append(render_job) cls.job_queue.append(render_job)
if force_start: if force_start:
@@ -89,7 +87,7 @@ class RenderQueue:
@classmethod @classmethod
def jobs_with_status(cls, status, priority_sorted=False): def jobs_with_status(cls, status, priority_sorted=False):
found_jobs = [x for x in cls.all_jobs() if x.render_status() == status] found_jobs = [x for x in cls.all_jobs() if x.status == status]
if priority_sorted: if priority_sorted:
found_jobs = sorted(found_jobs, key=lambda a: a.priority, reverse=False) found_jobs = sorted(found_jobs, key=lambda a: a.priority, reverse=False)
return found_jobs return found_jobs
@@ -103,7 +101,7 @@ class RenderQueue:
@classmethod @classmethod
def clear_history(cls): def clear_history(cls):
to_remove = [x for x in cls.all_jobs() if x.render_status() in [RenderStatus.CANCELLED, to_remove = [x for x in cls.all_jobs() if x.status in [RenderStatus.CANCELLED,
RenderStatus.COMPLETED, RenderStatus.ERROR]] RenderStatus.COMPLETED, RenderStatus.ERROR]]
for job_to_remove in to_remove: for job_to_remove in to_remove:
cls.delete_job(job_to_remove) cls.delete_job(job_to_remove)
@@ -141,15 +139,14 @@ class RenderQueue:
@classmethod @classmethod
def start_job(cls, job): def start_job(cls, job):
logger.info('Starting {}render: {} - Priority {}'.format('scheduled ' if job.scheduled_start else '', job.name, logger.info(f'Starting render: {job.name} - Priority {job.priority}')
job.priority))
job.start() job.start()
@classmethod @classmethod
def cancel_job(cls, job): def cancel_job(cls, job):
logger.info(f'Cancelling job ID: {job.id}') logger.info(f'Cancelling job ID: {job.id}')
job.stop() job.stop()
return job.render_status() == RenderStatus.CANCELLED return job.status == RenderStatus.CANCELLED
@classmethod @classmethod
def delete_job(cls, job): def delete_job(cls, job):

View File

@@ -22,9 +22,10 @@ class AERenderWorker(BaseRenderWorker):
supported_extensions = ['.aep'] supported_extensions = ['.aep']
engine = AERender engine = AERender
def __init__(self, input_path, output_path, args=None): def __init__(self, input_path, output_path, priority=2, args=None, owner=None,
super(AERenderWorker, self).__init__(input_path=input_path, output_path=output_path, ignore_extensions=False, client=None, name=None):
args=args) super(AERenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args,
client=client, priority=priority, owner=owner, name=name)
self.comp = args.get('comp', None) self.comp = args.get('comp', None)
self.render_settings = args.get('render_settings', None) self.render_settings = args.get('render_settings', None)

View File

@@ -8,9 +8,13 @@ import json
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from sqlalchemy import Column, Integer, String, DateTime, JSON, event
from sqlalchemy.ext.declarative import declarative_base
import psutil import psutil
logger = logging.getLogger() logger = logging.getLogger()
Base = declarative_base()
class RenderStatus(Enum): class RenderStatus(Enum):
@@ -30,11 +34,25 @@ def string_to_status(string):
return RenderStatus.ERROR return RenderStatus.ERROR
class BaseRenderWorker(object): class BaseRenderWorker(Base):
__tablename__ = 'render_workers'
id = Column(Integer, primary_key=True)
input_path = Column(String)
output_path = Column(String)
date_created = Column(DateTime)
renderer = Column(String)
renderer_version = Column(String)
priority = Column(Integer)
owner = Column(String)
client = Column(String)
name = Column(String)
file_hash = Column(String)
engine = None engine = None
def __init__(self, input_path, output_path, args=None, ignore_extensions=True): def __init__(self, input_path, output_path, priority=2, args=None, ignore_extensions=True, owner=None, client=None,
name=None):
if not ignore_extensions: if not ignore_extensions:
if not any(ext in input_path for ext in self.engine.supported_extensions): if not any(ext in input_path for ext in self.engine.supported_extensions):
@@ -49,7 +67,12 @@ class BaseRenderWorker(object):
self.output_path = output_path self.output_path = output_path
self.args = args or {} self.args = args or {}
self.date_created = datetime.now() self.date_created = datetime.now()
self.renderer = self.engine.name()
self.renderer_version = self.engine.version() self.renderer_version = self.engine.version()
self.priority = priority
self.owner = owner
self.client = client
self.name = name
# Frame Ranges # Frame Ranges
self.total_frames = 0 self.total_frames = 0

View File

@@ -13,9 +13,10 @@ class BlenderRenderWorker(BaseRenderWorker):
engine = Blender engine = Blender
def __init__(self, input_path, output_path, args=None): def __init__(self, input_path, output_path, priority=2, args=None, owner=None,
super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path, client=None, name=None):
ignore_extensions=False, args=args) super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args,
client=client, priority=priority, owner=owner, name=name)
# Args # Args
self.blender_engine = self.args.get('engine', 'BLENDER_EEVEE').upper() self.blender_engine = self.args.get('engine', 'BLENDER_EEVEE').upper()

View File

@@ -8,9 +8,10 @@ class FFMPEGRenderWorker(BaseRenderWorker):
engine = FFMPEG engine = FFMPEG
def __init__(self, input_path, output_path, args=None): def __init__(self, input_path, output_path, priority=2, args=None, owner=None,
super(FFMPEGRenderWorker, self).__init__(input_path=input_path, output_path=output_path, ignore_extensions=True, client=None, name=None):
args=args) super(FFMPEGRenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args,
client=client, priority=priority, owner=owner, name=name)
stream_info = subprocess.check_output([self.engine.renderer_path(), "-i", # https://stackoverflow.com/a/61604105 stream_info = subprocess.check_output([self.engine.renderer_path(), "-i", # https://stackoverflow.com/a/61604105
input_path, "-map", "0:v:0", "-c", "copy", "-f", "null", "-y", input_path, "-map", "0:v:0", "-c", "copy", "-f", "null", "-y",

View File

@@ -10,9 +10,11 @@ class RenderWorkerFactory:
return classes return classes
@staticmethod @staticmethod
def create_worker(renderer, input_path, output_path, args=None): def create_worker(renderer, input_path, output_path, priority=2, args=None, owner=None,
client=None, name=None):
worker_class = RenderWorkerFactory.class_for_name(renderer) worker_class = RenderWorkerFactory.class_for_name(renderer)
return worker_class(input_path=input_path, output_path=output_path, args=args) return worker_class(input_path=input_path, output_path=output_path, args=args, priority=priority, owner=owner,
client=client, name=name)
@staticmethod @staticmethod
def supported_renderers(): def supported_renderers():

View File

@@ -34,7 +34,7 @@ def sorted_jobs(all_jobs, sort_by_date=True):
sorted_job_list = [] sorted_job_list = []
if all_jobs: if all_jobs:
for status_category in categories: for status_category in categories:
found_jobs = [x for x in all_jobs if x.render_status() == status_category.value] found_jobs = [x for x in all_jobs if x.status == status_category.value]
if found_jobs: if found_jobs:
sorted_found_jobs = sorted(found_jobs, key=lambda d: d.date_created, reverse=True) sorted_found_jobs = sorted(found_jobs, key=lambda d: d.date_created, reverse=True)
sorted_job_list.extend(sorted_found_jobs) sorted_job_list.extend(sorted_found_jobs)
@@ -61,11 +61,11 @@ def job_detail(job_id):
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 is-fullwidth"') table_attributes='class="table is-narrow is-striped is-fullwidth"')
media_url = None media_url = None
if found_job.file_list() and found_job.render_status() == RenderStatus.COMPLETED: if found_job.file_list() and found_job.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,
hostname=RenderQueue.hostname, job_status=found_job.render_status().value.title(), hostname=RenderQueue.hostname, job_status=found_job.status.value.title(),
job=found_job, renderer_info=renderer_info()) job=found_job, renderer_info=renderer_info())
@@ -79,20 +79,20 @@ 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') and \ if not os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS') and \
found_job.render_status() not in [RenderStatus.CANCELLED, RenderStatus.ERROR]: found_job.status not in [RenderStatus.CANCELLED, RenderStatus.ERROR]:
generate_thumbnail_for_job(found_job, thumb_video_path, thumb_image_path, max_width=240) 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")
elif os.path.exists(thumb_image_path): elif os.path.exists(thumb_image_path):
return send_file(thumb_image_path, mimetype='image/jpeg') return send_file(thumb_image_path, mimetype='image/jpeg')
elif found_job.render_status() == RenderStatus.RUNNING: elif found_job.status == RenderStatus.RUNNING:
return send_file('static/images/gears.png', mimetype="image/png") return send_file('static/images/gears.png', mimetype="image/png")
elif found_job.render_status() == RenderStatus.CANCELLED: elif found_job.status == RenderStatus.CANCELLED:
return send_file('static/images/cancelled.png', mimetype="image/png") return send_file('static/images/cancelled.png', mimetype="image/png")
elif found_job.render_status() == RenderStatus.SCHEDULED: elif found_job.status == RenderStatus.SCHEDULED:
return send_file('static/images/scheduled.png', mimetype="image/png") return send_file('static/images/scheduled.png', mimetype="image/png")
elif found_job.render_status() == RenderStatus.NOT_STARTED: elif found_job.status == RenderStatus.NOT_STARTED:
return send_file('static/images/not_started.png', mimetype="image/png") return send_file('static/images/not_started.png', mimetype="image/png")
return send_file('static/images/error.png', mimetype="image/png") return send_file('static/images/error.png', mimetype="image/png")
@@ -329,8 +329,13 @@ def add_job(job_params, remove_job_dir_on_failure=False):
if client == RenderQueue.hostname: if client == RenderQueue.hostname:
logger.info(f"Creating job locally - {name if name else input_path}") logger.info(f"Creating job locally - {name if name else input_path}")
try: try:
render_job = ScheduledJob(renderer, input_path, output_path, args, priority, job_owner, client, render_job = RenderWorkerFactory.create_worker(renderer=renderer, input_path=input_path,
notify=False, custom_id=custom_id, name=name) output_path=output_path, args=args)
render_job.client = client
render_job.owner = job_owner
render_job.name = name
render_job.priority = priority
RenderQueue.add_to_render_queue(render_job, force_start=force_start) RenderQueue.add_to_render_queue(render_job, force_start=force_start)
return render_job.json() return render_job.json()
except Exception as e: except Exception as e:

View File

@@ -17,7 +17,7 @@
<br>Time Elapsed: <span id="time-elapsed">{{job.worker_data()['time_elapsed']}}</span> <br>Time Elapsed: <span id="time-elapsed">{{job.worker_data()['time_elapsed']}}</span>
</span> </span>
<script> <script>
var startingStatus = '{{job.render_status().value}}'; var startingStatus = '{{job.status.value}}';
function update_job() { function update_job() {
$.getJSON('/api/job/{{job.id}}', function(data) { $.getJSON('/api/job/{{job.id}}', function(data) {
document.getElementById('progress-bar').value = (data.percent_complete * 100); document.getElementById('progress-bar').value = (data.percent_complete * 100);

View File

@@ -36,7 +36,7 @@ def generate_thumbnail_for_job(job, thumb_video_path, thumb_image_path, max_widt
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
if job.render_status() == RenderStatus.COMPLETED: # use finished file for thumb if job.status == RenderStatus.COMPLETED: # use finished file for thumb
source_path = job.file_list() source_path = job.file_list()
else: else:
source_path = [job.input_path] # use source if nothing else source_path = [job.input_path] # use source if nothing else