Skip to content

Commit

Permalink
[cuegui] Allow previewing checkpointed frames (AcademySoftwareFoundat…
Browse files Browse the repository at this point in the history
…ion#1491)

Some Digital Content Creation tools (DCCs) provide a preview of the
images being rendered using a local server. This feature allows
capturing a preview on a tmp directory for displaying using the
configured viewer.

For this to work, the host running the frame being previewed needs to
have the port being used by the DCC accessible. The hostname is used as
the URL and the port is captured from the frame logs, as DCCs with this
feature usually output the port in the first lines of the log file.

One last change on this PR is to make sure both Preview and View Output
options are only displayed when the viewer cmd pattern is configured on
`cuegui.yaml`.
  • Loading branch information
DiegoTavares authored Aug 22, 2024
1 parent 195a003 commit 0e4cee0
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 22 deletions.
1 change: 1 addition & 0 deletions cuegui/cuegui/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ def __packaged_version():
OUTPUT_VIEWER_ACTION_TEXT = __config.get('output_viewer.action_text')
OUTPUT_VIEWER_EXTRACT_ARGS_REGEX = __config.get('output_viewer.extract_args_regex')
OUTPUT_VIEWER_CMD_PATTERN = __config.get('output_viewer.cmd_pattern')
OUTPUT_VIEWER_DIRECT_CMD_CALL = __config.get('output_viewer.direct_cmd_call')
OUTPUT_VIEWER_STEREO_MODIFIERS = __config.get('output_viewer.stereo_modifiers')
FINISHED_JOBS_READONLY_FRAME = __config.get('finished_jobs_readonly.frame', False)
FINISHED_JOBS_READONLY_LAYER = __config.get('finished_jobs_readonly.layer', False)
Expand Down
6 changes: 4 additions & 2 deletions cuegui/cuegui/FrameMonitorTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,8 @@ def __init__(self, widget, filterSelectedLayersCallback, readonly=False):
if bool(int(self.app.settings.value("AllowDeeding", 0))):
self.__menuActions.frames().addAction(self, "useLocalCores")

self.__menuActions.frames().addAction(self, "viewOutput")
if cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN:
self.__menuActions.frames().addAction(self, "viewOutput")

if self.app.applicationName() == "CueCommander":
self.__menuActions.frames().addAction(self, "viewHost")
Expand All @@ -933,7 +934,8 @@ def __init__(self, widget, filterSelectedLayersCallback, readonly=False):
filterSelectedLayersCallback, "stock-filters")
self.__menuActions.frames().addAction(self, "reorder").setEnabled(not readonly)
self.addSeparator()
self.__menuActions.frames().addAction(self, "previewMain")
if cuegui.Constants.OUTPUT_VIEWER_DIRECT_CMD_CALL:
self.__menuActions.frames().addAction(self, "previewMain")
self.__menuActions.frames().addAction(self, "previewAovs")
self.addSeparator()
self.__menuActions.frames().addAction(self, "retry").setEnabled(not readonly)
Expand Down
7 changes: 4 additions & 3 deletions cuegui/cuegui/JobMonitorTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,9 +416,10 @@ def contextMenuEvent(self, e):
if bool(int(self.app.settings.value("AllowDeeding", 0))):
self.__menuActions.jobs().addAction(menu, "useLocalCores")

it_view_action = self.__menuActions.jobs().addAction(menu, "viewOutput")
it_view_action.setDisabled(__count == 0)
it_view_action.setToolTip("Open Viewer for the selected items")
if cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN:
viewer_action = self.__menuActions.jobs().addAction(menu, "viewOutput")
viewer_action.setDisabled(__count == 0)
viewer_action.setToolTip("Open Viewer for the selected items")

depend_menu = QtWidgets.QMenu("&Dependencies",self)
self.__menuActions.jobs().addAction(depend_menu, "viewDepends")
Expand Down
3 changes: 2 additions & 1 deletion cuegui/cuegui/LayerMonitorTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ def contextMenuEvent(self, e):
menu = QtWidgets.QMenu()

self.__menuActions.layers().addAction(menu, "view")
self.__menuActions.layers().addAction(menu, "viewOutput")
if cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN:
self.__menuActions.layers().addAction(menu, "viewOutput")
depend_menu = QtWidgets.QMenu("&Dependencies", self)
self.__menuActions.layers().addAction(depend_menu, "viewDepends")
self.__menuActions.layers().addAction(depend_menu, "dependWizard")
Expand Down
56 changes: 41 additions & 15 deletions cuegui/cuegui/PreviewWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,8 @@
from __future__ import print_function
from __future__ import division

# pylint: disable=wrong-import-position
from future import standard_library
standard_library.install_aliases()
# pylint: enable=wrong-import-position

import os
import subprocess
import tempfile
import time
import urllib.error
Expand All @@ -36,6 +32,7 @@
from qtpy import QtCore
from qtpy import QtWidgets

import cuegui.Constants
import cuegui.Logger
import cuegui.Utils

Expand Down Expand Up @@ -66,13 +63,15 @@ def __init__(self, job, frame, aovs=False, parent=None):

self.__previewThread = None
# pylint: disable=unused-private-member
self.__itvFile = None
self.__previewFile = None

layout = QtWidgets.QVBoxLayout(self)

self.__msg = QtWidgets.QLabel("Waiting for preview images...", self)
self.__progbar = QtWidgets.QProgressBar(self)

self.closeEvent = self.__close

layout.addWidget(self.__msg)
layout.addWidget(self.__progbar)

Expand All @@ -86,15 +85,18 @@ def process(self):
if self.__aovs:
aovs = "/aovs"

playlist = urllib.request.urlopen("http://%s:%d%s" % (http_host, http_port, aovs)).read()
url = "http://%s:%d%s" % (http_host, http_port, aovs)
with urllib.request.urlopen(url) as response:
playlist = response.read()

for element in Et.fromstring(playlist).findall("page/edit/element"):
items.append(element.text)
items.append(str(element.text))

if not items:
return

# pylint: disable=unused-private-member
self.__itvFile = self.__writePlaylist(playlist)
self.__previewFile = self.__writePlaylist(playlist)
self.__previewThread = PreviewProcessorWatchThread(items, self)
self.app.threads.append(self.__previewThread)
self.__previewThread.start()
Expand All @@ -109,6 +111,18 @@ def updateProgressDialog(self, current, max_progress):
self.__progbar.setValue(current)
else:
self.close()
self.__previewThread.stop()
self.__launchViewer()

def __launchViewer(self):
"""Launch a viewer for this preview frame"""
if not cuegui.Constants.OUTPUT_VIEWER_DIRECT_CMD_CALL:
print("No viewer configured. "
"Please ensure output_viewer.direct_cmd_call is configured properly")
print("Launching preview: ", self.__previewFile)
cmd = cuegui.Constants.OUTPUT_VIEWER_DIRECT_CMD_CALL.format(
paths=self.__previewFile).split()
subprocess.call(cmd, shell=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def processTimedOut(self):
"""Event handler when the process has timed out."""
Expand All @@ -120,6 +134,7 @@ def processTimedOut(self):

@staticmethod
def __writePlaylist(data):
"""Write preview data to a temporary file"""
(fh, name) = tempfile.mkstemp(suffix=".itv", prefix="playlist")
os.close(fh)
with open(name, "w", encoding='utf-8') as fp:
Expand All @@ -129,7 +144,13 @@ def __writePlaylist(data):
fp.close()
return name

def __close(self, event):
"""Close preview thread"""
del event
self.__previewThread.terminate = True

def __findHttpPort(self):
"""Figure out what port is being used by the tool to write previews"""
log = cuegui.Utils.getFrameLogFile(self.__job, self.__frame)
with open(log, "r", encoding='utf-8') as fp:
try:
Expand All @@ -138,12 +159,12 @@ def __findHttpPort(self):
counter += 1
if counter >= 5000:
break
if line.startswith("Preview Server"):
return int(line.split(":")[1].strip())
if "Preview Server" in line[:30]:
return int(line.split(":")[-1].strip())
finally:
fp.close()

raise Exception("Katana 2.7.19 and above is required for preview feature.")
raise Exception("This frame doesn't support previews. No Preview Server found.")


class PreviewProcessorWatchThread(QtCore.QThread):
Expand All @@ -153,26 +174,31 @@ class PreviewProcessorWatchThread(QtCore.QThread):
serious filer problems.
"""
existCountChanged = QtCore.Signal(int, int)
timeout = QtCore.Signal()

def __init__(self, items, parent=None):
QtCore.QThread.__init__(self, parent)
self.__items = items
self.__timeout = 60 + (30 * len(items))
self.terminate = False

def run(self):
"""
Just check to see how many files exist and
emit that back to our parent.
"""
start_time = time.time()
while 1:
while not self.terminate:
count = len([path for path in self.__items if os.path.exists(path)])
self.emit(QtCore.SIGNAL('existCountChanged(int, int)'), count, len(self.__items))
self.existsCountChanged.emit(count, len(self.__items))
self.existCountChanged.emit(count, len(self.__items))
if count == len(self.__items):
break
time.sleep(1)
if time.time() > self.__timeout + start_time:
self.timeout.emit()
logger.warning('Timed out waiting for preview server.')
break

def stop(self):
"""Stop the preview capture thread"""
self.terminate = True
7 changes: 6 additions & 1 deletion cuegui/cuegui/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,12 @@ def showErrorMessageBox(text, title="ERROR!", detailedText=None):
def shutdownThread(thread):
"""Shuts down a WorkerThread."""
thread.stop()
return thread.wait(1500)
# Stop may terminate the underlying thread object yielding a
# RuntimeError(QtFatal) when wait is called
try:
return thread.wait(1500)
except RuntimeError:
return False

def getLLU(item):
""" LLU time from log_path """
Expand Down
2 changes: 2 additions & 0 deletions cuegui/cuegui/config/cuegui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ memory_warning_level: 5242880
# # if extract_args_regex is not provided, cmd_pattern is called directly with paths as arguments
# extract_args_regex: '/shots/(?P<show>\w+)/(?P<shot>shot\w+)/.*'
# cmd_pattern: "env SHOW={show} SHOT={shot} COLOR_IO=/{show}/home/colorspaces.xml OCIO=/{show}/home/config.ocio openrv {paths}"
# # Pattern to call viewer cmd directly without extracting environment variables. Used for previewing frames
# direct_cmd_call: "openrv {paths}"

# # if provided, paths containing any of the two values are considered the same output and only one
# # of them will be passed to the viewer
Expand Down

0 comments on commit 0e4cee0

Please sign in to comment.