From fb154ad4f18b43b6d7e872f239889bd98381d00a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 16:11:55 +0100 Subject: [PATCH] OP-4361 - implemented multiple output modules per composition Working version for local rendering. --- openpype/hosts/aftereffects/api/extension.zxp | Bin 101376 -> 101426 bytes .../api/extension/jsx/hostscript.jsx | 176 +++++++++--------- openpype/hosts/aftereffects/api/ws_stub.py | 5 +- .../plugins/publish/collect_render.py | 43 +++-- .../plugins/publish/extract_local_render.py | 64 ++++--- 5 files changed, 151 insertions(+), 137 deletions(-) diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index f80787fef8a989c46d4fda489a39b7792ff9500b..b436f0ca0b67313cf4509d1ec5e7d4a1dfd2d106 100644 GIT binary patch delta 8603 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 diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index cdaa2e3b83..9b211207de 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -395,13 +395,84 @@ function saveAs(path){ app.project.save(fp = new File(path)); } +function getRenderInfo(comp_id){ + /*** + Get info from render queue. + Currently pulls only file name to parse extension and + if it is sequence in Python + Args: + comp_id (int): id of composition + Return: + (list) [{file_name:"xx.png", width:00, height:00}] + **/ + var item = app.project.itemByID(comp_id); + if (!item){ + return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)") + } + + var comp_name = item.name; + var output_metadata = [] + try{ + // render_item.duplicate() should create new item on renderQueue + // BUT it works only sometimes, there are some weird synchronization issue + // this method will be called always before render, so prepare items here + // for render to spare the hassle + for (i = 1; i <= app.project.renderQueue.numItems; ++i){ + var render_item = app.project.renderQueue.item(i); + if (render_item.comp.id != comp_id){ + continue; + } + + if (render_item.status == RQItemStatus.DONE){ + render_item.duplicate(); // create new, cannot change status if DONE + render_item.remove(); // remove existing to limit duplications + continue; + } + } + + // properly validate as `numItems` won't change magically + var comp_id_count = 0; + for (i = 1; i <= app.project.renderQueue.numItems; ++i){ + var render_item = app.project.renderQueue.item(i); + if (render_item.comp.id != comp_id){ + continue; + } + comp_id_count += 1; + var item = render_item.outputModule(1); + + for (j = 1; j<= render_item.numOutputModules; ++j){ + var file_url = item.file.toString(); + output_metadata.push( + JSON.stringify({ + "file_name": file_url, + "width": render_item.comp.width, + "height": render_item.comp.height + }) + ); + } + } + } catch (error) { + return _prepareError("There is no render queue, create one"); + } + + if (comp_id_count > 1){ + return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!") + } + + if (comp_id_count == 0){ + return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.") + } + + return '[' + output_metadata.join() + ']'; +} + function getAudioUrlForComp(comp_id){ /** * Searches composition for audio layer - * + * * Only single AVLayer is expected! * Used for collecting Audio - * + * * Args: * comp_id (int): id of composition * Return: @@ -429,7 +500,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ /** * Adds already imported FootageItem ('item_id') as a new * layer to composition ('comp_id'). - * + * * Args: * comp_id (int): id of target composition * item_id (int): FootageItem.id @@ -452,17 +523,17 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ function importBackground(comp_id, composition_name, files_to_import){ /** * Imports backgrounds images to existing or new composition. - * + * * If comp_id is not provided, new composition is created, basic * values (width, heights, frameRatio) takes from first imported * image. - * + * * Args: * comp_id (int): id of existing composition (null if new) - * composition_name (str): used when new composition + * composition_name (str): used when new composition * files_to_import (list): list of absolute paths to import and * add as layers - * + * * Returns: * (str): json representation (id, name, members) */ @@ -484,7 +555,7 @@ function importBackground(comp_id, composition_name, files_to_import){ } } } - + if (files_to_import){ for (i = 0; i < files_to_import.length; ++i){ item = _importItem(files_to_import[i]); @@ -496,8 +567,8 @@ function importBackground(comp_id, composition_name, files_to_import){ if (!comp){ folder = app.project.items.addFolder(composition_name); imported_ids.push(folder.id); - comp = app.project.items.addComp(composition_name, item.width, - item.height, item.pixelAspect, + comp = app.project.items.addComp(composition_name, item.width, + item.height, item.pixelAspect, 1, 26.7); // hardcode defaults imported_ids.push(comp.id); comp.parentFolder = folder; @@ -506,7 +577,7 @@ function importBackground(comp_id, composition_name, files_to_import){ item.parentFolder = folder; addItemAsLayerToComp(comp.id, item.id, comp); - } + } } var item = {"name": comp.name, "id": folder.id, @@ -517,19 +588,19 @@ function importBackground(comp_id, composition_name, files_to_import){ function reloadBackground(comp_id, composition_name, files_to_import){ /** * Reloads existing composition. - * + * * It deletes complete composition with encompassing folder, recreates * from scratch via 'importBackground' functionality. - * + * * Args: * comp_id (int): id of existing composition (null if new) - * composition_name (str): used when new composition + * composition_name (str): used when new composition * files_to_import (list): list of absolute paths to import and * add as layers - * + * * Returns: * (str): json representation (id, name, members) - * + * */ var imported_ids = []; // keep track of members of composition comp = app.project.itemByID(comp_id); @@ -592,7 +663,7 @@ function reloadBackground(comp_id, composition_name, files_to_import){ function _get_file_name(file_url){ /** * Returns file name without extension from 'file_url' - * + * * Args: * file_url (str): full absolute url * Returns: @@ -607,7 +678,7 @@ function _delete_obsolete_items(folder, new_filenames){ /*** * Goes through 'folder' and removes layers not in new * background - * + * * Args: * folder (FolderItem) * new_filenames (array): list of layer names in new bg @@ -632,14 +703,14 @@ function _delete_obsolete_items(folder, new_filenames){ function _importItem(file_url){ /** * Imports 'file_url' as new FootageItem - * + * * Args: * file_url (str): file url with content * Returns: * (FootageItem) */ file_name = _get_file_name(file_url); - + //importFile prepared previously to return json item_json = importFile(file_url, file_name, JSON.stringify({"ImportAsType":"FOOTAGE"})); item_json = JSON.parse(item_json); @@ -661,71 +732,6 @@ function isFileSequence (item){ return false; } -function getRenderInfo(comp_id){ - /*** - Get info from render queue. - Currently pulls only file name to parse extension and - if it is sequence in Python - **/ - var item = app.project.itemByID(comp_id); - if (!item){ - return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)") - } - - var comp_name = item.name; - try{ - // render_item.duplicate() should create new item on renderQueue - // BUT it works only sometimes, there are some weird synchronization issue - // this method will be called always before render, so prepare items here - // for render to spare the hassle - for (i = 1; i <= app.project.renderQueue.numItems; ++i){ - var render_item = app.project.renderQueue.item(i); - if (render_item.comp.id != comp_id){ - continue; - } - - if (render_item.status == RQItemStatus.DONE){ - render_item.duplicate(); // create new, cannot change status if DONE - render_item.remove(); // remove existing to limit duplications - continue; - } - } - - // properly validate as `numItems` won't change magically - var comp_id_count = 0; - for (i = 1; i <= app.project.renderQueue.numItems; ++i){ - var render_item = app.project.renderQueue.item(i); - if (render_item.comp.id != comp_id){ - continue; - } - comp_id_count += 1; - var item = render_item.outputModule(1); - } - } catch (error) { - return _prepareError("There is no render queue, create one"); - } - - if (comp_id_count > 1){ - return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!") - } - - if (comp_id_count == 0){ - return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.") - } - - if (render_item.numOutputModules !=1){ - return _prepareError("There must be just 1 Output Module in Render Queue for '" + comp_name + "'! Keep only correct one.") - } - - var file_url = item.file.toString(); - - return JSON.stringify({ - "file_name": file_url, - "width": render_item.comp.width, - "height": render_item.comp.height - }) -} - function render(target_folder, comp_id){ var out_dir = new Folder(target_folder); var out_dir = out_dir.fsName; diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 32125a7d99..e5d6d9ed89 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -422,15 +422,14 @@ class AfterEffectsServerStub(): """ Get render queue info for render purposes Returns: - (AEItem): with 'file_name' field + (list) of (AEItem): with 'file_name' field """ res = self.websocketserver.call(self.client.call ('AfterEffects.get_render_info', comp_id=comp_id)) records = self._to_records(self._handle_return(res)) - if records: - return records.pop() + return records def get_audio_url(self, item_id): """ Get audio layer absolute url for comp diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 2b37c1f101..6153a426cf 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -22,7 +22,7 @@ class AERenderInstance(RenderInstance): stagingDir = attr.ib(default=None) app_version = attr.ib(default=None) publish_attributes = attr.ib(default={}) - file_name = attr.ib(default=None) + file_names = attr.ib(default=[]) class CollectAERender(publish.AbstractCollectRender): @@ -86,6 +86,7 @@ class CollectAERender(publish.AbstractCollectRender): render_q = CollectAERender.get_stub().get_render_info(comp_id) if not render_q: raise ValueError("No file extension set in Render Queue") + render_item = render_q[0] subset_name = inst.data["subset"] instance = AERenderInstance( @@ -102,8 +103,8 @@ class CollectAERender(publish.AbstractCollectRender): setMembers='', publish=True, name=subset_name, - resolutionWidth=render_q.width, - resolutionHeight=render_q.height, + resolutionWidth=render_item.width, + resolutionHeight=render_item.height, pixelAspect=1, tileRendering=False, tilesX=0, @@ -114,7 +115,7 @@ class CollectAERender(publish.AbstractCollectRender): fps=fps, app_version=app_version, publish_attributes=inst.data.get("publish_attributes", {}), - file_name=render_q.file_name + file_names=[item.file_name for item in render_q] ) comp = compositions_by_id.get(comp_id) @@ -162,28 +163,30 @@ class CollectAERender(publish.AbstractCollectRender): start = render_instance.frameStart end = render_instance.frameEnd - _, ext = os.path.splitext(os.path.basename(render_instance.file_name)) - base_dir = self._get_output_dir(render_instance) expected_files = [] - if "#" not in render_instance.file_name: # single frame (mov)W - path = os.path.join(base_dir, "{}_{}_{}.{}".format( - render_instance.asset, - render_instance.subset, - "v{:03d}".format(render_instance.version), - ext.replace('.', '') - )) - expected_files.append(path) - else: - for frame in range(start, end + 1): - path = os.path.join(base_dir, "{}_{}_{}.{}.{}".format( + for file_name in render_instance.file_names: + _, ext = os.path.splitext(os.path.basename(file_name)) + ext = ext.replace('.', '') + version_str = "v{:03d}".format(render_instance.version) + if "#" not in file_name: # single frame (mov)W + path = os.path.join(base_dir, "{}_{}_{}.{}".format( render_instance.asset, render_instance.subset, - "v{:03d}".format(render_instance.version), - str(frame).zfill(self.padding_width), - ext.replace('.', '') + version_str, + ext )) expected_files.append(path) + else: + for frame in range(start, end + 1): + path = os.path.join(base_dir, "{}_{}_{}.{}.{}".format( + render_instance.asset, + render_instance.subset, + version_str, + str(frame).zfill(self.padding_width), + ext + )) + expected_files.append(path) return expected_files def _get_output_dir(self, render_instance): diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 309855f1c7..d535329eb4 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -24,46 +24,52 @@ class ExtractLocalRender(publish.Extractor): self.log.debug("staging_dir::{}".format(staging_dir)) # pull file name collected value from Render Queue Output module - if not instance.data["file_name"]: + if not instance.data["file_names"]: raise ValueError("No file extension set in Render Queue") comp_id = instance.data['comp_id'] stub.render(staging_dir, comp_id) - _, ext = os.path.splitext(os.path.basename(instance.data["file_name"])) - ext = ext[1:] + representations = [] + for file_name in instance.data["file_names"]: + _, ext = os.path.splitext(os.path.basename(file_name)) + ext = ext[1:] - first_file_path = None - files = [] - for file_name in os.listdir(staging_dir): - if not file_name.endswith(ext): - continue + first_file_path = None + files = [] + for found_file_name in os.listdir(staging_dir): + if not found_file_name.endswith(ext): + continue - files.append(file_name) - if first_file_path is None: - first_file_path = os.path.join(staging_dir, - file_name) + files.append(found_file_name) + if first_file_path is None: + first_file_path = os.path.join(staging_dir, + found_file_name) - if not files: - self.log.info("no files") - return + if not files: + self.log.info("no files") + return - resulting_files = files - if len(files) == 1: - resulting_files = files[0] + # single file cannot be wrapped in array + resulting_files = files + if len(files) == 1: + resulting_files = files[0] - repre_data = { - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - "name": ext, - "ext": ext, - "files": resulting_files, - "stagingDir": staging_dir - } - if instance.data["review"]: - repre_data["tags"] = ["review"] + repre_data = { + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "name": ext, + "ext": ext, + "files": resulting_files, + "stagingDir": staging_dir + } + first_repre = not representations + if instance.data["review"] and first_repre: + repre_data["tags"] = ["review"] - instance.data["representations"] = [repre_data] + representations.append(repre_data) + + instance.data["representations"] = representations ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") # Generate thumbnail.