6 Commits

Author SHA1 Message Date
Brett Williams
a3e2fa7e07 Update the renderer to reflect the current file type 2023-11-17 08:18:07 -06:00
Brett Williams
23901bc8e4 Fix add_job crashing 2023-11-16 14:36:09 -06:00
Brett Williams
ba996c58f5 Make sure supported_extensions is now called as a method everywhere 2023-11-16 14:01:48 -06:00
Brett Williams
9e8eb77328 Cleanup extension matching 2023-11-16 13:55:49 -06:00
Brett Williams
81d2cb70b8 Cleanup unnecessary code in FFMPEG 2023-11-16 11:27:30 -06:00
Brett Williams
6dc8db2d8c Make sure progress UI updates occur on main thread 2023-11-16 06:13:12 -06:00
8 changed files with 59 additions and 37 deletions

View File

@@ -423,10 +423,12 @@ def renderer_info():
# Get all installed versions of engine # Get all installed versions of engine
installed_versions = EngineManager.all_versions_for_engine(engine.name()) installed_versions = EngineManager.all_versions_for_engine(engine.name())
if installed_versions: if installed_versions:
install_path = installed_versions[0]['path'] # fixme: using system versions only because downloaded versions may have permissions issues
system_installed_versions = [x for x in installed_versions if x['type'] == 'system']
install_path = system_installed_versions[0]['path'] if system_installed_versions else installed_versions[0]['path']
renderer_data[engine.name()] = {'is_available': RenderQueue.is_available_for_job(engine.name()), renderer_data[engine.name()] = {'is_available': RenderQueue.is_available_for_job(engine.name()),
'versions': installed_versions, 'versions': installed_versions,
'supported_extensions': engine.supported_extensions, 'supported_extensions': engine.supported_extensions(),
'supported_export_formats': engine(install_path).get_output_formats()} 'supported_export_formats': engine(install_path).get_output_formats()}
return renderer_data return renderer_data

View File

@@ -10,7 +10,6 @@ logger = logging.getLogger()
class Blender(BaseRenderEngine): class Blender(BaseRenderEngine):
install_paths = ['/Applications/Blender.app/Contents/MacOS/Blender'] install_paths = ['/Applications/Blender.app/Contents/MacOS/Blender']
supported_extensions = ['.blend']
binary_names = {'linux': 'blender', 'windows': 'blender.exe', 'macos': 'Blender'} binary_names = {'linux': 'blender', 'windows': 'blender.exe', 'macos': 'Blender'}
@staticmethod @staticmethod
@@ -23,6 +22,10 @@ class Blender(BaseRenderEngine):
from src.engines.blender.blender_worker import BlenderRenderWorker from src.engines.blender.blender_worker import BlenderRenderWorker
return BlenderRenderWorker return BlenderRenderWorker
@staticmethod
def supported_extensions():
return ['blend']
def version(self): def version(self):
version = None version = None
try: try:

View File

@@ -47,7 +47,7 @@ class BaseRenderEngine(object):
def worker_class(): # override when subclassing to link worker class def worker_class(): # override when subclassing to link worker class
raise NotImplementedError("Worker class not implemented") raise NotImplementedError("Worker class not implemented")
def get_help(self): def get_help(self): # override if renderer uses different help flag
path = self.renderer_path() path = self.renderer_path()
if not path: if not path:
raise FileNotFoundError("renderer path not found") raise FileNotFoundError("renderer path not found")
@@ -56,7 +56,7 @@ class BaseRenderEngine(object):
return help_doc return help_doc
def get_project_info(self, project_path, timeout=10): def get_project_info(self, project_path, timeout=10):
raise NotImplementedError(f"get_project_info not implemented for {cls.__name__}") raise NotImplementedError(f"get_project_info not implemented for {self.__name__}")
@classmethod @classmethod
def get_output_formats(cls): def get_output_formats(cls):
@@ -66,6 +66,9 @@ class BaseRenderEngine(object):
def get_arguments(cls): def get_arguments(cls):
pass pass
def get_options(self): # override to return options for ui
return {}
def perform_presubmission_tasks(self, project_path): def perform_presubmission_tasks(self, project_path):
return project_path return project_path

View File

@@ -47,7 +47,7 @@ class BaseRenderWorker(Base):
name=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()):
err_meg = f'Cannot find valid project with supported file extension for {self.engine.name()} renderer' err_meg = f'Cannot find valid project with supported file extension for {self.engine.name()} renderer'
logger.error(err_meg) logger.error(err_meg)
raise ValueError(err_meg) raise ValueError(err_meg)

View File

@@ -244,10 +244,11 @@ class EngineManager:
@classmethod @classmethod
def engine_for_project_path(cls, path): def engine_for_project_path(cls, path):
name, extension = os.path.splitext(path) name, extension = os.path.splitext(path)
extension = extension.lower().strip('.')
for engine in cls.supported_engines(): for engine in cls.supported_engines():
if extension in engine.supported_extensions: if extension in engine.supported_extensions():
return engine return engine
undefined_renderer_support = [x for x in cls.supported_engines() if not x.supported_extensions] undefined_renderer_support = [x for x in cls.supported_engines() if not x.supported_extensions()]
return undefined_renderer_support[0] return undefined_renderer_support[0]

View File

@@ -18,6 +18,16 @@ class FFMPEG(BaseRenderEngine):
from src.engines.ffmpeg.ffmpeg_worker import FFMPEGRenderWorker from src.engines.ffmpeg.ffmpeg_worker import FFMPEGRenderWorker
return FFMPEGRenderWorker return FFMPEGRenderWorker
@classmethod
def supported_extensions(cls):
help_text = (subprocess.check_output([cls().renderer_path(), '-h', 'full'], stderr=subprocess.STDOUT)
.decode('utf-8'))
found = re.findall('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)
def version(self): def version(self):
version = None version = None
try: try:
@@ -31,15 +41,11 @@ class FFMPEG(BaseRenderEngine):
return version return version
def get_project_info(self, project_path, timeout=10): def get_project_info(self, project_path, timeout=10):
return self.get_video_info_ffprobe(project_path)
@staticmethod
def get_video_info_ffprobe(video_path):
try: try:
# Run ffprobe and parse the output as JSON # Run ffprobe and parse the output as JSON
cmd = [ cmd = [
'ffprobe', '-v', 'quiet', '-print_format', 'json', 'ffprobe', '-v', 'quiet', '-print_format', 'json',
'-show_streams', '-select_streams', 'v', video_path '-show_streams', '-select_streams', 'v', project_path
] ]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
video_info = json.loads(result.stdout) video_info = json.loads(result.stdout)
@@ -85,7 +91,7 @@ class FFMPEG(BaseRenderEngine):
try: try:
formats_raw = subprocess.check_output([self.renderer_path(), '-formats'], stderr=subprocess.DEVNULL, formats_raw = subprocess.check_output([self.renderer_path(), '-formats'], stderr=subprocess.DEVNULL,
timeout=SUBPROCESS_TIMEOUT).decode('utf-8') timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
pattern = '(?P<type>[DE]{1,2})\s+(?P<id>\S{2,})\s+(?P<name>.*)\r' pattern = '(?P<type>[DE]{1,2})\s+(?P<id>\S{2,})\s+(?P<name>.*)'
all_formats = [m.groupdict() for m in re.finditer(pattern, formats_raw)] all_formats = [m.groupdict() for m in re.finditer(pattern, formats_raw)]
return all_formats return all_formats
except Exception as e: except Exception as e:
@@ -105,7 +111,7 @@ class FFMPEG(BaseRenderEngine):
return found_extensions return found_extensions
def get_output_formats(self): def get_output_formats(self):
return [x for x in self.get_all_formats() if 'E' in x['type'].upper()] return [x['id'] for x in self.get_all_formats() if 'E' in x['type'].upper()]
def get_frame_count(self, path_to_file): def get_frame_count(self, path_to_file):
raw_stdout = subprocess.check_output([self.renderer_path(), '-i', path_to_file, '-map', '0:v:0', '-c', 'copy', raw_stdout = subprocess.check_output([self.renderer_path(), '-i', path_to_file, '-map', '0:v:0', '-c', 'copy',
@@ -117,7 +123,8 @@ class FFMPEG(BaseRenderEngine):
return frame_number return frame_number
def get_arguments(self): def get_arguments(self):
help_text = subprocess.check_output([self.renderer_path(), '-h', 'long'], stderr=subprocess.STDOUT).decode('utf-8') help_text = (subprocess.check_output([self.renderer_path(), '-h', 'long'], stderr=subprocess.STDOUT)
.decode('utf-8'))
lines = help_text.splitlines() lines = help_text.splitlines()
options = {} options = {}

View File

@@ -10,15 +10,9 @@ class FFMPEGRenderWorker(BaseRenderWorker):
engine = FFMPEG engine = FFMPEG
def __init__(self, input_path, output_path, args=None, parent=None, name=None): def __init__(self, input_path, output_path, engine_path, args=None, parent=None, name=None):
super(FFMPEGRenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args, super(FFMPEGRenderWorker, self).__init__(input_path=input_path, output_path=output_path,
parent=parent, name=name) engine_path=engine_path, args=args, parent=parent, name=name)
stream_info = subprocess.check_output([self.renderer_path, "-i", # https://stackoverflow.com/a/61604105
input_path, "-map", "0:v:0", "-c", "copy", "-f", "null", "-y",
"/dev/null"], stderr=subprocess.STDOUT).decode('utf-8')
found_frames = re.findall('frame=\s*(\d+)', stream_info)
self.project_length = found_frames[-1] if found_frames else '-1'
self.current_frame = -1 self.current_frame = -1
def generate_worker_subprocess(self): def generate_worker_subprocess(self):

View File

@@ -5,7 +5,7 @@ import socket
import threading import threading
import psutil import psutil
from PyQt6.QtCore import QThread, pyqtSignal, Qt from PyQt6.QtCore import QThread, pyqtSignal, Qt, pyqtSlot
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QSpinBox, QComboBox, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QSpinBox, QComboBox,
QGroupBox, QCheckBox, QProgressBar, QPlainTextEdit, QDoubleSpinBox, QMessageBox, QListWidget, QListWidgetItem QGroupBox, QCheckBox, QProgressBar, QPlainTextEdit, QDoubleSpinBox, QMessageBox, QListWidget, QListWidgetItem
@@ -234,8 +234,13 @@ class NewRenderJobForm(QWidget):
self.toggle_renderer_enablement(False) self.toggle_renderer_enablement(False)
def update_renderer_info(self): def update_renderer_info(self):
# get the renderer info and add them all to the ui
self.renderer_info = self.server_proxy.get_renderer_info() self.renderer_info = self.server_proxy.get_renderer_info()
self.renderer_type.addItems(self.renderer_info.keys()) self.renderer_type.addItems(self.renderer_info.keys())
# select the best renderer for the file type
engine = EngineManager.engine_for_project_path(self.project_path)
self.renderer_type.setCurrentText(engine.name().lower())
# refresh ui
self.renderer_changed() self.renderer_changed()
def renderer_changed(self): def renderer_changed(self):
@@ -294,9 +299,13 @@ class NewRenderJobForm(QWidget):
# Set the best renderer we can find # Set the best renderer we can find
input_path = self.scene_file_input.text() input_path = self.scene_file_input.text()
engine = EngineManager.engine_for_project_path(input_path) engine = EngineManager.engine_for_project_path(input_path)
index = self.renderer_type.findText(engine.name().lower())
if index >= 0: engine_index = self.renderer_type.findText(engine.name().lower())
self.renderer_type.setCurrentIndex(index) if engine_index >= 0:
self.renderer_type.setCurrentIndex(engine_index)
else:
self.renderer_type.setCurrentIndex(0) #todo: find out why we don't have renderer info yet
# not ideal but if we don't have the renderer info we have to pick something
self.output_path_input.setText(os.path.basename(input_path)) self.output_path_input.setText(os.path.basename(input_path))
@@ -331,10 +340,7 @@ class NewRenderJobForm(QWidget):
self.cameras_group.setHidden(True) self.cameras_group.setHidden(True)
# Dynamic Engine Options # Dynamic Engine Options
engine_name = self.renderer_type.currentText() clear_layout(self.renderer_options_layout) # clear old options
engine = EngineManager.engine_with_name(engine_name)
# clear old options
clear_layout(self.renderer_options_layout)
# dynamically populate option list # dynamically populate option list
self.current_engine_options = engine().get_options() self.current_engine_options = engine().get_options()
for option in self.current_engine_options: for option in self.current_engine_options:
@@ -402,14 +408,23 @@ class NewRenderJobForm(QWidget):
# submit job in background thread # submit job in background thread
self.worker_thread = SubmitWorker(window=self) self.worker_thread = SubmitWorker(window=self)
self.worker_thread.update_ui_signal.connect(self.update_submit_progress)
self.worker_thread.message_signal.connect(self.after_job_submission) self.worker_thread.message_signal.connect(self.after_job_submission)
self.worker_thread.start() self.worker_thread.start()
@pyqtSlot(str, str)
def update_submit_progress(self, hostname, percent):
# Update the UI here. This slot will be executed in the main thread
self.submit_progress_label.setText(f"Transferring to {hostname} - {percent}%")
self.submit_progress.setMaximum(100)
self.submit_progress.setValue(int(percent))
class SubmitWorker(QThread): class SubmitWorker(QThread):
"""Worker class called to submit all the jobs to the server and update the UI accordingly""" """Worker class called to submit all the jobs to the server and update the UI accordingly"""
message_signal = pyqtSignal(Response) message_signal = pyqtSignal(Response)
update_ui_signal = pyqtSignal(str, str)
def __init__(self, window): def __init__(self, window):
super().__init__() super().__init__()
@@ -421,10 +436,7 @@ class SubmitWorker(QThread):
def callback(monitor): def callback(monitor):
percent = f"{monitor.bytes_read / encoder_len * 100:.0f}" percent = f"{monitor.bytes_read / encoder_len * 100:.0f}"
self.window.submit_progress_label.setText(f"Transferring to {hostname} - {percent}%") self.update_ui_signal.emit(hostname, percent)
self.window.submit_progress.setMaximum(100)
self.window.submit_progress.setValue(int(percent))
return callback return callback
hostname = self.window.server_input.currentText() hostname = self.window.server_input.currentText()