14 Commits

Author SHA1 Message Date
Brett Williams
a76b0340f9 Disable parts of add_job UI when missing critical project data instead of crashing 2024-08-04 12:06:29 -05:00
Brett Williams
f9c114bf32 Add UI options for aerender 2024-08-04 10:00:16 -05:00
Brett Williams
dad9b8c250 Misc improvements 2024-08-04 01:19:22 -05:00
Brett Williams
8826382f86 Misc worker cleanup 2024-08-04 01:03:24 -05:00
Brett Williams
58822c4a20 get_project_info gets comp names from .aepx files now 2024-08-04 01:01:21 -05:00
Brett Williams
dc7f3877b2 Cleanup subprocess generation 2024-08-03 21:23:25 -05:00
Brett Williams
b1280ad445 Add AERender to supported_engines in EngineManager 2024-08-03 21:03:01 -05:00
Brett Williams
0cebb93ba2 Merge remote-tracking branch 'origin/feature/84-after-effects-support' into feature/84-after-effects-support 2024-08-03 20:56:54 -05:00
Brett Williams
a2785400ac Fix generate_worker_subprocess in aerender_worker.py 2024-08-03 20:55:48 -05:00
Brett Williams
d9201b5082 Changes to engine file extensions structure 2024-08-03 20:55:48 -05:00
Brett Williams
7d633d97c2 Fix getting path to After Effects 2024-08-03 20:55:48 -05:00
Brett Williams
e6e2ff8e07 Fix generate_worker_subprocess in aerender_worker.py 2024-08-03 20:45:43 -05:00
Brett Williams
7986960b21 Changes to engine file extensions structure 2024-08-03 20:21:26 -05:00
Brett Williams
ebb847b09e Fix getting path to After Effects 2024-08-03 20:00:36 -05:00
15 changed files with 189 additions and 413 deletions

View File

@@ -1,279 +0,0 @@
#!/usr/bin/env python3
import datetime
import os.path
import socket
import threading
import time
import traceback
from rich import box
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.panel import Panel
from rich.table import Column
from rich.table import Table
from rich.text import Text
from rich.tree import Tree
from src.engines.core.base_worker import RenderStatus, string_to_status
from src.api.server_proxy import RenderServerProxy
from src.utilities.misc_helper import get_time_elapsed
from start_server import start_server
"""
The RenderDashboard is designed to be run on a remote machine or on the local server
This provides a detailed status of all jobs running on the server
"""
status_colors = {RenderStatus.ERROR: "red", RenderStatus.CANCELLED: 'orange1', RenderStatus.COMPLETED: 'green',
RenderStatus.NOT_STARTED: "yellow", RenderStatus.SCHEDULED: 'purple',
RenderStatus.RUNNING: 'cyan'}
categories = [RenderStatus.RUNNING, RenderStatus.ERROR, RenderStatus.NOT_STARTED, RenderStatus.SCHEDULED,
RenderStatus.COMPLETED, RenderStatus.CANCELLED, RenderStatus.UNDEFINED]
renderer_colors = {'ffmpeg': '[magenta]', 'blender': '[orange1]', 'aerender': '[purple]'}
local_hostname = socket.gethostname()
def status_string_to_color(status_string):
job_status = string_to_status(status_string)
job_color = '[{}]'.format(status_colors[job_status])
return job_color
def sorted_jobs(all_jobs):
sort_by_date = True
if not sort_by_date:
sorted_job_list = []
if all_jobs:
for status_category in categories:
found_jobs = [x for x in all_jobs if x['status'] == status_category.value]
if found_jobs:
sorted_found_jobs = sorted(found_jobs, key=lambda d: datetime.datetime.fromisoformat(d['date_created']), reverse=True)
sorted_job_list.extend(sorted_found_jobs)
else:
sorted_job_list = sorted(all_jobs, key=lambda d: datetime.datetime.fromisoformat(d['date_created']), reverse=True)
return sorted_job_list
def create_node_tree(all_server_data) -> Tree:
main_tree = Tree("[magenta]Server Cluster")
for server_host, server_data in all_server_data['servers'].items():
node_title_local = f"[cyan bold]{server_host}[/] [yellow](This Computer)[default]"
node_title_remote = f"[cyan]{server_host} [magenta](Remote)[default]"
node_tree_text = node_title_local if (server_host == local_hostname) else node_title_remote
if server_data.get('is_online', False):
node_tree_text = node_tree_text + " - [green]Running"
node_tree = Tree(node_tree_text)
stats_text = f"CPU: [yellow]{server_data['status']['cpu_percent']}% [default]| RAM: " \
f"[yellow]{server_data['status']['memory_percent']}% [default]| Cores: " \
f"[yellow]{server_data['status']['cpu_count']} [default]| " \
f"{server_data['status']['platform'].split('-')[0]}"
node_tree.add(Tree(stats_text))
running_jobs = [job for job in server_data['jobs'] if job['status'] == RenderStatus.RUNNING.value]
not_started = [job for job in server_data['jobs'] if job['status'] == RenderStatus.NOT_STARTED.value]
scheduled = [job for job in server_data['jobs'] if job['status'] == RenderStatus.SCHEDULED.value]
jobs_to_display = running_jobs + not_started + scheduled
jobs_tree = Tree(f"Running: [green]{len(running_jobs)} [default]| Queued: [cyan]{len(not_started)}"
f"[default] | Scheduled: [cyan]{len(scheduled)}")
for job in jobs_to_display:
renderer = f"{renderer_colors[job['renderer']]}{job['renderer']}[default]"
filename = os.path.basename(job['input_path']).split('.')[0]
if job['status'] == RenderStatus.RUNNING.value:
jobs_tree.add(f"[bold]{renderer} {filename} ({job['id']}) - {status_string_to_color(job['status'])}{(float(job['percent_complete']) * 100):.1f}%")
else:
jobs_tree.add(f"{filename} ({job['id']}) - {status_string_to_color(job['status'])}{job['status'].title()}")
if not jobs_to_display:
jobs_tree.add("[italic]No running jobs")
node_tree.add(jobs_tree)
main_tree.add(node_tree)
else:
# if server is offline
node_tree_text = node_tree_text + " - [red]Offline"
node_tree = Tree(node_tree_text)
main_tree.add(node_tree)
return main_tree
def create_jobs_table(all_server_data) -> Table:
table = Table("ID", "Name", "Renderer", Column(header="Priority", justify="center"),
Column(header="Status", justify="center"), Column(header="Time Elapsed", justify="right"),
Column(header="# Frames", justify="right"), "Client", show_lines=True,
box=box.HEAVY_HEAD)
all_jobs = []
for server_name, server_data in all_server_data['servers'].items():
for job in server_data['jobs']:
#todo: clean this up
all_jobs.append(job)
all_jobs = sorted_jobs(all_jobs)
for job in all_jobs:
job_status = string_to_status(job['status'])
job_color = '[{}]'.format(status_colors[job_status])
job_text = f"{job_color}" + job_status.value.title()
if job_status == RenderStatus.ERROR and job['errors']:
job_text = job_text + "\n" + "\n".join(job['errors'])
# Project name
project_name = job_color + (job['name'] or os.path.basename(job['input_path']))
elapsed_time = get_time_elapsed(datetime.datetime.fromisoformat(job['start_time']),
datetime.datetime.fromisoformat(job['end_time']))
if job_status == RenderStatus.RUNNING:
job_text = f"{job_color}[bold]Running - {float(job['percent_complete']) * 100:.1f}%"
elapsed_time = "[bold]" + elapsed_time
project_name = "[bold]" + project_name
elif job_status == RenderStatus.CANCELLED or job_status == RenderStatus.ERROR:
project_name = "[strike]" + project_name
# Priority
priority_color = ["red", "yellow", "cyan"][(job['priority'] - 1)]
client_name = job['client'] or 'unknown'
client_colors = {'unknown': '[red]', local_hostname: '[yellow]'}
client_title = client_colors.get(client_name, '[magenta]') + client_name
table.add_row(
job['id'],
project_name,
renderer_colors.get(job['renderer'], '[cyan]') + job['renderer'] + '[default]-' + job['renderer_version'],
f"[{priority_color}]{job['priority']}",
job_text,
elapsed_time,
str(max(int(job['total_frames']), 1)),
client_title
)
return table
def create_status_panel(all_server_data):
for key, value in all_server_data['servers'].items():
if key == local_hostname:
return str(value['status'])
return "no status"
class KeyboardThread(threading.Thread):
def __init__(self, input_cbk = None, name='keyboard-input-thread'):
self.input_cbk = input_cbk
super(KeyboardThread, self).__init__(name=name)
self.start()
def run(self):
while True:
self.input_cbk(input()) #waits to get input + Return
def my_callback(inp):
#evaluate the keyboard input
print('You Entered:', inp)
if __name__ == '__main__':
get_server_ip = input("Enter server IP or None for local: ") or local_hostname
server_proxy = RenderServerProxy(get_server_ip, "8080")
if not server_proxy.connect():
if server_proxy.hostname == local_hostname:
start_server_input = input("Local server not running. Start server? (y/n) ")
if start_server_input and start_server_input[0].lower() == "y":
# Startup the local server
start_server()
test = server_proxy.connect()
print(f"connected? {test}")
else:
print(f"\nUnable to connect to server: {server_proxy.hostname}")
print("\nVerify IP address is correct and server is running")
exit(1)
# start the Keyboard thread
# kthread = KeyboardThread(my_callback)
# Console Layout
console = Console()
layout = Layout()
# Divide the "screen" in to three parts
layout.split(
Layout(name="header", size=3),
Layout(ratio=1, name="main")
# Layout(size=10, name="footer"),
)
# Divide the "main" layout in to "side" and "body"
layout["main"].split_row(
Layout(name="side"),
Layout(name="body",
ratio=3))
# Divide the "side" layout in to two
layout["side"].split(Layout(name="side_top"), Layout(name="side_bottom"))
# Server connection header
header_text = Text(f"Connected to server: ")
header_text.append(f"{server_proxy.hostname} ", style="green")
if server_proxy.hostname == local_hostname:
header_text.append("(This Computer)", style="yellow")
else:
header_text.append("(Remote)", style="magenta")
# background process to update server data independent of the UI
def fetch_server_data(server):
while True:
fetched_data = server.get_data(timeout=5)
if fetched_data:
server.fetched_status_data = fetched_data
time.sleep(1)
x = threading.Thread(target=fetch_server_data, args=(server_proxy,))
x.daemon = True
x.start()
# draw and update the UI
with Live(console=console, screen=False, refresh_per_second=1, transient=True) as live:
while True:
try:
if server_proxy.fetched_status_data:
server_online = False
if server_proxy.fetched_status_data.get('timestamp', None):
timestamp = datetime.datetime.fromisoformat(server_proxy.fetched_status_data['timestamp'])
time_diff = datetime.datetime.now() - timestamp
server_online = time_diff.seconds < 10 # client is offline if not updated in certain time
layout["body"].update(create_jobs_table(server_proxy.fetched_status_data))
layout["side_top"].update(Panel(create_node_tree(server_proxy.fetched_status_data)))
layout["side_bottom"].update(Panel(create_status_panel(server_proxy.fetched_status_data)))
online_text = "Online" if server_online else "Offline"
online_color = "green" if server_online else "red"
layout["header"].update(Panel(Text(f"Zordon Render Client - Version 0.0.1 alpha - {online_text}",
justify="center", style=online_color)))
live.update(layout, refresh=False)
except Exception as e:
print(f"Exception updating table: {e}")
traceback.print_exception(e)
time.sleep(1)
# # # todo: Add input prompt to manage running jobs (ie add, cancel, get info, etc)

View File

@@ -11,7 +11,13 @@ from setuptools import setup
APP = ['main.py']
DATA_FILES = [('config', glob.glob('config/*.*')),
('resources', glob.glob('resources/*.*'))]
OPTIONS = {}
OPTIONS = {
'excludes': ['PySide6'],
'includes': ['zeroconf', 'zeroconf._services.info'],
'plist': {
'LSMinimumSystemVersion': '10.15', # Specify minimum macOS version
},
}
setup(
app=APP,

View File

@@ -463,6 +463,7 @@ def delete_engine_download():
@server.get('/api/renderer/<renderer>/args')
def get_renderer_args(renderer):
try:
# todo: possibly deprecate
renderer_engine_class = EngineManager.engine_with_name(renderer)
return renderer_engine_class().get_arguments()
except LookupError:

View File

@@ -1,22 +1,74 @@
import glob
import logging
import subprocess
from src.engines.core.base_engine import BaseRenderEngine
logger = logging.getLogger()
class AERender(BaseRenderEngine):
supported_extensions = ['.aep']
file_extensions = ['aepx']
def version(self):
version = None
try:
render_path = self.renderer_path()
if render_path:
ver_out = subprocess.check_output([render_path, '-version'], timeout=SUBPROCESS_TIMEOUT)
version = ver_out.decode('utf-8').split(" ")[-1].strip()
ver_out = subprocess.run([render_path, '-version'], capture_output=True, text=True)
version = ver_out.stdout.split(" ")[-1].strip()
except Exception as e:
logger.error(f'Failed to get {self.name()} version: {e}')
return version
@classmethod
def default_renderer_path(cls):
paths = glob.glob('/Applications/*After Effects*/aerender')
if len(paths) > 1:
logger.warning('Multiple After Effects installations detected')
elif not paths:
logger.error('After Effects installation not found')
return paths[0]
def get_project_info(self, project_path, timeout=10):
scene_info = {}
try:
import xml.etree.ElementTree as ET
tree = ET.parse(project_path)
root = tree.getroot()
namespace = {'ae': 'http://www.adobe.com/products/aftereffects'}
comp_names = []
for item in root.findall(".//ae:Item", namespace):
if item.find("ae:Layr", namespace) is not None:
for string in item.findall("./ae:string", namespace):
comp_names.append(string.text)
scene_info['comp_names'] = comp_names
except Exception as e:
logger.error(f'Error getting file details for .aepx file: {e}')
return scene_info
def run_javascript(self, script_path, project_path, timeout=None):
# todo: implement
pass
@classmethod
def get_output_formats(cls):
# todo: create implementation
return []
def ui_options(self, project_info):
from src.engines.aerender.aerender_ui import AERenderUI
return AERenderUI.get_options(self, project_info)
@classmethod
def worker_class(cls):
from src.engines.aerender.aerender_worker import AERenderWorker
return AERenderWorker
if __name__ == "__main__":
x = AERender().get_project_info('/Users/brett/ae_testing/project.aepx')
print(x)

View File

@@ -0,0 +1,8 @@
class AERenderUI:
@staticmethod
def get_options(instance, project_info):
options = [
{'name': 'comp', 'options': project_info.get('comp_names', [])}
]
return options

View File

@@ -9,72 +9,39 @@ import time
from src.engines.core.base_worker import BaseRenderWorker, timecode_to_frames
from src.engines.aerender.aerender_engine import AERender
def aerender_path():
paths = glob.glob('/Applications/*After Effects*/aerender')
if len(paths) > 1:
logging.warning('Multiple After Effects installations detected')
elif not paths:
logging.error('After Effects installation not found')
else:
return paths[0]
logger = logging.getLogger()
class AERenderWorker(BaseRenderWorker):
supported_extensions = ['.aep']
engine = AERender
def __init__(self, input_path, output_path, args=None, parent=None, name=None):
super(AERenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args,
parent=parent, name=name)
def __init__(self, input_path, output_path, engine_path, args=None, parent=None, name=None):
super(AERenderWorker, self).__init__(input_path=input_path, output_path=output_path, engine_path=engine_path,
args=args, parent=parent, name=name)
self.comp = args.get('comp', None)
self.render_settings = args.get('render_settings', None)
self.omsettings = args.get('omsettings', None)
self.progress = 0
self.progress_history = []
self.attributes = {}
# temp files for processing stdout
self.__progress_history = []
self.__temp_attributes = {}
def generate_worker_subprocess(self):
if os.path.exists('nexrender-cli-macos'):
logging.info('nexrender found')
# {
# "template": {
# "src": String,
# "composition": String,
#
# "frameStart": Number,
# "frameEnd": Number,
# "frameIncrement": Number,
#
# "continueOnMissing": Boolean,
# "settingsTemplate": String,
# "outputModule": String,
# "outputExt": String,
# },
# "assets": [],
# "actions": {
# "prerender": [],
# "postrender": [],
# },
# "onChange": Function,
# "onRenderProgress": Function
# }
job = {'template':
{
'src': 'file://' + self.input_path, 'composition': self.comp.replace('"', ''),
'settingsTemplate': self.render_settings.replace('"', ''),
'outputModule': self.omsettings.replace('"', ''), 'outputExt': 'mov'}
}
x = ['./nexrender-cli-macos', "'{}'".format(json.dumps(job))]
else:
logging.info('nexrender not found')
x = [aerender_path(), '-project', self.input_path, '-comp', self.comp, '-RStemplate', self.render_settings,
'-OMtemplate', self.omsettings, '-output', self.output_path]
return x
comp = self.args.get('comp', 'Comp 1')
render_settings = self.args.get('render_settings', None)
omsettings = self.args.get('omsettings', None)
command = [self.renderer_path, '-project', self.input_path, '-comp', f'"{comp}"']
if render_settings:
command.extend(['-RStemplate', render_settings])
if omsettings:
command.extend(['-OMtemplate', omsettings])
command.extend(['-s', self.start_frame,
'-e', self.end_frame,
'-output', self.output_path])
return command
def _parse_stdout(self, line):
@@ -83,12 +50,12 @@ class AERenderWorker(BaseRenderWorker):
# print 'progress'
trimmed = line.replace('PROGRESS:', '').strip()
if len(trimmed):
self.progress_history.append(line)
self.__progress_history.append(line)
if 'Seconds' in trimmed:
self._update_progress(line)
elif ': ' in trimmed:
tmp = trimmed.split(': ')
self.attributes[tmp[0].strip()] = tmp[1].strip()
self.__temp_attributes[tmp[0].strip()] = tmp[1].strip()
elif line.startswith('WARNING:'):
trimmed = line.replace('WARNING:', '').strip()
self.warnings.append(trimmed)
@@ -99,28 +66,28 @@ class AERenderWorker(BaseRenderWorker):
def _update_progress(self, line):
if not self.total_frames:
duration_string = self.attributes.get('Duration', None)
frame_rate = self.attributes.get('Frame Rate', '0').split(' ')[0]
duration_string = self.__temp_attributes.get('Duration', None)
frame_rate = self.__temp_attributes.get('Frame Rate', '0').split(' ')[0]
self.total_frames = timecode_to_frames(duration_string.split('Duration:')[-1], float(frame_rate))
match = re.match(r'PROGRESS:.*\((?P<frame>\d+)\): (?P<time>\d+)', line).groupdict()
self.last_frame = match['frame']
self.current_frame = match['frame']
def average_frame_duration(self):
total_durations = 0
for line in self.progress_history:
for line in self.__progress_history:
match = re.match(r'PROGRESS:.*\((?P<frame>\d+)\): (?P<time>\d+)', line)
if match:
total_durations += int(match.group(2))
average = float(total_durations) / self.last_frame
average = float(total_durations) / self.current_frame
return average
def percent_complete(self):
if self.total_frames:
return (float(self.last_frame) / float(self.total_frames)) * 100
return (float(self.current_frame) / float(self.total_frames)) * 100
else:
return 0
@@ -128,8 +95,11 @@ class AERenderWorker(BaseRenderWorker):
if __name__ == '__main__':
logging.basicConfig(format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S', level=logging.DEBUG)
r = AERenderWorker('/Users/brett/Desktop/Youtube_Vids/Film_Formats/Frame_Animations.aep', '"Film Pan"',
'"Draft Settings"', '"ProRes"', '/Users/brett/Desktop/test_render')
r = AERenderWorker(input_path='/Users/brett/ae_testing/project.aepx',
output_path='/Users/brett/ae_testing/project.mp4',
engine_path=AERenderWorker.engine.default_renderer_path(),
args={'start_frame': 1, 'end_frame': 5})
r.start()
while r.is_running():
time.sleep(0.1)

View File

@@ -11,25 +11,22 @@ class Blender(BaseRenderEngine):
install_paths = ['/Applications/Blender.app/Contents/MacOS/Blender']
binary_names = {'linux': 'blender', 'windows': 'blender.exe', 'macos': 'Blender'}
file_extensions = ['blend']
@staticmethod
def downloader():
from src.engines.blender.blender_downloader import BlenderDownloader
return BlenderDownloader
@staticmethod
def worker_class():
@classmethod
def worker_class(cls):
from src.engines.blender.blender_worker import BlenderRenderWorker
return BlenderRenderWorker
def ui_options(self):
def ui_options(self, project_info):
from src.engines.blender.blender_ui import BlenderUI
return BlenderUI.get_options(self)
@staticmethod
def supported_extensions():
return ['blend']
def version(self):
version = None
try:
@@ -115,7 +112,7 @@ class Blender(BaseRenderEngine):
logger.error(f'Error packing .blend file: {e}')
return None
def get_arguments(self):
def get_arguments(self): # possibly deprecate
help_text = subprocess.check_output([self.renderer_path(), '-h']).decode('utf-8')
lines = help_text.splitlines()

View File

@@ -12,17 +12,12 @@ class BlenderRenderWorker(BaseRenderWorker):
engine = Blender
def __init__(self, input_path, output_path, engine_path, args=None, parent=None, name=None):
super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args, parent=parent, name=name)
super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path,
engine_path=engine_path, args=args, parent=parent, name=name)
# Stats
self.__frame_percent_complete = 0.0
# Scene Info
self.scene_info = Blender(engine_path).get_project_info(input_path)
self.start_frame = int(self.scene_info.get('start_frame', 1))
self.end_frame = int(self.scene_info.get('end_frame', self.start_frame))
self.project_length = (self.end_frame - self.start_frame) + 1
self.current_frame = -1
self.current_frame = -1 # todo: is this necessary?
def generate_worker_subprocess(self):

View File

@@ -19,8 +19,8 @@ for cam_obj in bpy.data.cameras:
data = {'cameras': cameras,
'engine': scene.render.engine,
'frame_start': scene.frame_start,
'frame_end': scene.frame_end,
'start_frame': scene.frame_start,
'end_frame': scene.frame_end,
'resolution_x': scene.render.resolution_x,
'resolution_y': scene.render.resolution_y,
'resolution_percentage': scene.render.resolution_percentage,

View File

@@ -9,12 +9,12 @@ SUBPROCESS_TIMEOUT = 5
class BaseRenderEngine(object):
install_paths = []
supported_extensions = []
file_extensions = []
def __init__(self, custom_path=None):
self.custom_renderer_path = custom_path
if not self.renderer_path() or not os.path.exists(self.renderer_path()):
raise FileNotFoundError(f"Cannot find path to renderer for {self.name()} instance")
raise FileNotFoundError(f"Cannot find path ({self.renderer_path()}) for renderer '{self.name()}'")
if not os.access(self.renderer_path(), os.X_OK):
logger.warning(f"Path is not executable. Setting permissions to 755 for {self.renderer_path()}")
@@ -47,19 +47,18 @@ class BaseRenderEngine(object):
def downloader(): # override when subclassing if using a downloader class
return None
@staticmethod
def worker_class(): # override when subclassing to link worker class
raise NotImplementedError("Worker class not implemented")
@classmethod
def worker_class(cls): # override when subclassing to link worker class
raise NotImplementedError(f"Worker class not implemented for engine {cls.name()}")
def ui_options(self): # override to return options for ui
def ui_options(self, project_info): # override to return options for ui
return {}
def get_help(self): # override if renderer uses different help flag
path = self.renderer_path()
if not path:
raise FileNotFoundError("renderer path not found")
help_doc = subprocess.check_output([path, '-h'], stderr=subprocess.STDOUT,
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
help_doc = subprocess.run([path, '-h'], capture_output=True, text=True).stdout.strip()
return help_doc
def get_project_info(self, project_path, timeout=10):
@@ -69,6 +68,10 @@ class BaseRenderEngine(object):
def get_output_formats(cls):
raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}")
@classmethod
def supported_extensions(cls):
return cls.file_extensions
def get_arguments(self):
pass

View File

@@ -81,8 +81,11 @@ class BaseRenderWorker(Base):
# Frame Ranges
self.project_length = 0 # is this necessary?
self.current_frame = 0
self.start_frame = 0
self.end_frame = None
# Get Project Info
self.scene_info = self.engine(engine_path).get_project_info(project_path=input_path)
self.start_frame = int(self.scene_info.get('start_frame', 1))
self.end_frame = int(self.scene_info.get('end_frame', self.start_frame))
# Logging
self.start_time = None
@@ -192,7 +195,7 @@ class BaseRenderWorker(Base):
f.write(f"{self.start_time.isoformat()} - Starting {self.engine.name()} {self.renderer_version} "
f"render for {self.input_path}\n\n")
f.write(f"Running command: {subprocess_cmds}\n")
f.write(f"Running command: \"{' '.join(subprocess_cmds)}\"\n")
f.write('=' * 80 + '\n\n')
while True:
@@ -207,9 +210,9 @@ class BaseRenderWorker(Base):
else:
f.write(f'\n{"=" * 20} Attempt #{failed_attempts + 1} {"=" * 20}\n\n')
logger.warning(f"Restarting render - Attempt #{failed_attempts + 1}")
self.status = RenderStatus.RUNNING
# Start process and get updates
self.status = RenderStatus.RUNNING
self.__process = subprocess.Popen(subprocess_cmds, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
universal_newlines=False)

View File

@@ -6,6 +6,7 @@ import concurrent.futures
from src.engines.blender.blender_engine import Blender
from src.engines.ffmpeg.ffmpeg_engine import FFMPEG
from src.engines.aerender.aerender_engine import AERender
from src.utilities.misc_helper import system_safe_path, current_system_os, current_system_cpu
logger = logging.getLogger()
@@ -18,7 +19,7 @@ class EngineManager:
@staticmethod
def supported_engines():
return [Blender, FFMPEG]
return [Blender, FFMPEG, AERender]
@classmethod
def engine_with_name(cls, engine_name):
@@ -79,17 +80,20 @@ class EngineManager:
'type': 'system'
}
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {
executor.submit(fetch_engine_details, eng): eng.name()
for eng in cls.supported_engines()
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
}
if not filter_name:
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {
executor.submit(fetch_engine_details, eng): eng.name()
for eng in cls.supported_engines()
if eng.default_renderer_path()
}
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
results.append(result)
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
results.append(result)
else:
results.append(fetch_engine_details(cls.engine_with_name(filter_name)))
return results
@@ -294,6 +298,6 @@ if __name__ == '__main__':
# print(EngineManager.newest_engine_version('blender', 'macos', 'arm64'))
# EngineManager.delete_engine_download('blender', '3.2.1', 'macos', 'a')
EngineManager.engines_path = "/Users/brettwilliams/zordon-uploads/engines"
EngineManager.engines_path = "/Users/brettwilliams/zordon-uploads/engines/"
# print(EngineManager.is_version_downloaded("ffmpeg", "6.0"))
print(EngineManager.get_engines())

View File

@@ -12,24 +12,26 @@ class FFMPEG(BaseRenderEngine):
from src.engines.ffmpeg.ffmpeg_downloader import FFMPEGDownloader
return FFMPEGDownloader
@staticmethod
def worker_class():
@classmethod
def worker_class(cls):
from src.engines.ffmpeg.ffmpeg_worker import FFMPEGRenderWorker
return FFMPEGRenderWorker
def ui_options(self):
def ui_options(self, project_info):
from src.engines.ffmpeg.ffmpeg_ui import FFMPEGUI
return FFMPEGUI.get_options(self)
return FFMPEGUI.get_options(self, project_info)
@classmethod
def supported_extensions(cls):
help_text = (subprocess.check_output([cls().renderer_path(), '-h', 'full'], stderr=subprocess.STDOUT)
.decode('utf-8'))
found = re.findall(r'extensions that .* is allowed to access \(default "(.*)"', help_text)
found_extensions = set()
for match in found:
found_extensions.update(match.split(','))
return list(found_extensions)
if not cls.file_extensions:
help_text = (subprocess.check_output([cls().renderer_path(), '-h', 'full'], stderr=subprocess.STDOUT)
.decode('utf-8'))
found = re.findall(r'extensions that .* is allowed to access \(default "(.*)"', help_text)
found_extensions = set()
for match in found:
found_extensions.update(match.split(','))
cls.file_extensions = list(found_extensions)
return cls.file_extensions
def version(self):
version = None

View File

@@ -1,5 +1,5 @@
class FFMPEGUI:
@staticmethod
def get_options(instance):
def get_options(instance, project_info):
options = []
return options

View File

@@ -322,12 +322,22 @@ class NewRenderJobForm(QWidget):
self.load_file_group.setHidden(True)
self.toggle_renderer_enablement(True)
# Load scene data
self.start_frame_input.setValue(self.project_info.get('frame_start'))
self.end_frame_input.setValue(self.project_info.get('frame_end'))
self.resolution_x_input.setValue(self.project_info.get('resolution_x'))
self.resolution_y_input.setValue(self.project_info.get('resolution_y'))
self.frame_rate_input.setValue(self.project_info.get('fps'))
# -- Load scene data
# start / end frames
self.start_frame_input.setValue(self.project_info.get('start_frame', 0))
self.end_frame_input.setValue(self.project_info.get('end_frame', 0))
self.start_frame_input.setEnabled(bool(self.project_info.get('start_frame')))
self.end_frame_input.setEnabled(bool(self.project_info.get('start_frame')))
# resolution
self.resolution_x_input.setValue(self.project_info.get('resolution_x', 1920))
self.resolution_y_input.setValue(self.project_info.get('resolution_y', 1080))
self.resolution_x_input.setEnabled(bool(self.project_info.get('resolution_x')))
self.resolution_y_input.setEnabled(bool(self.project_info.get('resolution_y')))
# frame rate
self.frame_rate_input.setValue(self.project_info.get('fps', 24))
self.frame_rate_input.setEnabled(bool(self.project_info.get('fps')))
# Cameras
self.cameras_list.clear()
@@ -350,7 +360,7 @@ class NewRenderJobForm(QWidget):
# Dynamic Engine Options
clear_layout(self.renderer_options_layout) # clear old options
# dynamically populate option list
self.current_engine_options = engine().ui_options()
self.current_engine_options = engine().ui_options(self.project_info)
for option in self.current_engine_options:
h_layout = QHBoxLayout()
label = QLabel(option['name'].replace('_', ' ').capitalize() + ':')
@@ -496,8 +506,12 @@ class SubmitWorker(QThread):
engine = EngineManager.engine_with_name(self.window.renderer_type.currentText().lower())
input_path = engine().perform_presubmission_tasks(input_path)
# submit
result = self.window.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list,
callback=create_callback)
result = None
try:
result = self.window.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list,
callback=create_callback)
except Exception as e:
pass
self.message_signal.emit(result)