diff --git a/pype/plugins/global/publish/extract_json.py b/pype/plugins/global/_publish_unused/extract_json.py similarity index 100% rename from pype/plugins/global/publish/extract_json.py rename to pype/plugins/global/_publish_unused/extract_json.py diff --git a/pype/plugins/global/publish/validate_ffmpeg_installed.py b/pype/plugins/global/publish/validate_ffmpeg_installed.py new file mode 100644 index 0000000000..250da0afd9 --- /dev/null +++ b/pype/plugins/global/publish/validate_ffmpeg_installed.py @@ -0,0 +1,28 @@ +import pyblish.api +import os +import subprocess + + +class ValidateFfmpegInstallef(pyblish.api.Validator): + """Validate availability of ffmpeg tool in PATH""" + + order = pyblish.api.ValidatorOrder + label = 'Validate ffmpeg installation' + families = ['review'] + optional = True + + def is_tool(self, name): + try: + devnull = open(os.devnull, "w") + subprocess.Popen( + [name], stdout=devnull, stderr=devnull + ).communicate() + except OSError as e: + if e.errno == os.errno.ENOENT: + return False + return True + + def process(self, instance): + if self.is_tool('ffmpeg') is False: + self.log.error("ffmpeg not found in PATH") + raise RuntimeError('ffmpeg not installed.') diff --git a/pype/plugins/maya/publish/collect_model.py b/pype/plugins/maya/publish/collect_model.py index fa6a0eee1c..831399339b 100644 --- a/pype/plugins/maya/publish/collect_model.py +++ b/pype/plugins/maya/publish/collect_model.py @@ -26,4 +26,7 @@ class CollectModelData(pyblish.api.InstancePlugin): instance.data['endFrame'] = frame # make ftrack publishable - instance.data["families"] = ['ftrack'] + if instance.data.get('families'): + instance.data['families'].append('ftrack') + else: + instance.data['families'] = ['ftrack'] diff --git a/pype/plugins/maya/publish/collect_review.py b/pype/plugins/maya/publish/collect_review.py index 6ddb550a99..9fb054127e 100644 --- a/pype/plugins/maya/publish/collect_review.py +++ b/pype/plugins/maya/publish/collect_review.py @@ -4,7 +4,7 @@ import pymel.core as pm import pyblish.api import avalon.api -class CollectReviewData(pyblish.api.InstancePlugin): +class CollectReview(pyblish.api.InstancePlugin): """Collect Review data """ @@ -15,12 +15,9 @@ class CollectReviewData(pyblish.api.InstancePlugin): def process(self, instance): - # make ftrack publishable - instance.data["families"] = ['ftrack'] - context = instance.context + self.log.debug('instance: {}'.format(instance)) task = avalon.api.Session["AVALON_TASK"] - # pseudo code # get cameras members = instance.data['setMembers'] @@ -33,7 +30,7 @@ class CollectReviewData(pyblish.api.InstancePlugin): camera = cameras[0] self.log.debug('camera: {}'.format(camera)) - objectset = context.data['objectsets'] + objectset = instance.context.data['objectsets'] reviewable_subset = None reviewable_subset = list(set(members) & set(objectset)) @@ -41,18 +38,34 @@ class CollectReviewData(pyblish.api.InstancePlugin): assert len(reviewable_subset) <= 1, "Multiple subsets for review" self.log.debug('subset for review: {}'.format(reviewable_subset)) - for inst in context: - self.log.debug('instance: {}'.format(instance)) + i = 0 + for inst in instance.context: + data = instance.context[i].data + if inst.name == reviewable_subset[0]: - if inst.data.get('families'): - inst.data['families'].append('review') + if data.get('families'): + data['families'].append('review') else: - inst.data['families'] = ['review'] - inst.data['review_camera'] = camera - self.log.info('adding review family to {}'.format(reviewable_subset)) + data['families'] = ['review'] + self.log.debug('adding review family to {}'.format(reviewable_subset)) + data['review_camera'] = camera + data["publish"] = False + data['startFrameReview'] = instance.data['startFrame'] + data['endFrameReview'] = instance.data['endFrame'] + data['handles'] = instance.data['handles'] + data['step'] = instance.data['step'] + data['fps'] = instance.data['fps'] cmds.setAttr(str(instance) + '.active', 0) - inst.data['publish'] = 0 - inst.data['active'] = 0 + i += 1 + instance.context[i].data.update(data) + instance.data['remove'] = True else: instance.data['subset'] = task + 'Review' instance.data['review_camera'] = camera + instance.data['startFrameReview'] = instance.data['startFrame'] + instance.data['endFrameReview'] = instance.data['endFrame'] + + # make ftrack publishable + instance.data["families"] = ['ftrack'] + + cmds.setAttr(str(instance) + '.active', 1) diff --git a/pype/plugins/maya/publish/extract_quicktime.py b/pype/plugins/maya/publish/extract_quicktime.py index 9883f5b264..16c8c08e52 100644 --- a/pype/plugins/maya/publish/extract_quicktime.py +++ b/pype/plugins/maya/publish/extract_quicktime.py @@ -1,38 +1,26 @@ import os +import subprocess import contextlib -import time -import sys import capture_gui import clique import pype.maya.lib as lib import pype.api +import avalon.maya -from maya import cmds +from maya import cmds, mel import pymel.core as pm from pype.vendor import ffmpeg reload(ffmpeg) -import avalon.maya - -# import maya_utils as mu - -# from tweakHUD import master -# from tweakHUD import draft_hud as dHUD -# from tweakHUD import ftrackStrings as fStrings - -# -# def soundOffsetFunc(oSF, SF, H): -# tmOff = (oSF - H) - SF -# return tmOff - +# TODO: move codec settings to presets class ExtractQuicktime(pype.api.Extractor): - """Extract a Camera as Alembic. + """Extract Quicktime from viewport capture. - The cameras gets baked to world space by default. Only when the instance's - `bakeToWorldSpace` is set to False it will include its full hierarchy. + Takes review camera and creates review Quicktime video based on viewport + capture. """ @@ -44,8 +32,17 @@ class ExtractQuicktime(pype.api.Extractor): def process(self, instance): self.log.info("Extracting capture..") - start = instance.data.get("startFrame", 1) - end = instance.data.get("endFrame", 25) + # get scene fps + fps = mel.eval('currentTimeUnitToFPS()') + + # if start and end frames cannot be determined, get them + # from Maya timeline + start = instance.data.get("startFrameReview") + end = instance.data.get("endFrameReview") + if start is None: + start = cmds.playbackOptions(query=True, animationStartTime=True) + if end is None: + end = cmds.playbackOptions(query=True, animationEndTime=True) self.log.info("start: {}, end: {}".format(start, end)) handles = instance.data.get("handles", 0) if handles: @@ -53,46 +50,7 @@ class ExtractQuicktime(pype.api.Extractor): end += handles # get cameras - members = instance.data['setMembers'] camera = instance.data['review_camera'] - # cameras = cmds.ls(members, leaf=True, shapes=True, long=True, - # dag=True, type="camera") - - # # validate required settings - # assert len(cameras) == 1, "Not a single camera found in extraction" - # camera = cameras[0] - - - # project_code = ftrack_data['Project']['code'] - # task_type = ftrack_data['Task']['type'] - # - # # load Preset - # studio_repos = os.path.abspath(os.environ.get('studio_repos')) - # shot_preset_path = os.path.join(studio_repos, 'maya', - # 'capture_gui_presets', - # (project_code + '_' + task_type + '_' + asset + '.json')) - # - # task_preset_path = os.path.join(studio_repos, 'maya', - # 'capture_gui_presets', - # (project_code + '_' + task_type + '.json')) - # - # project_preset_path = os.path.join(studio_repos, 'maya', - # 'capture_gui_presets', - # (project_code + '.json')) - # - # default_preset_path = os.path.join(studio_repos, 'maya', - # 'capture_gui_presets', - # 'default.json') - # - # if os.path.isfile(shot_preset_path): - # preset_to_use = shot_preset_path - # elif os.path.isfile(task_preset_path): - # preset_to_use = task_preset_path - # elif os.path.isfile(project_preset_path): - # preset_to_use = project_preset_path - # else: - # preset_to_use = default_preset_path - capture_preset = "" try: preset = lib.load_capture_preset(capture_preset) @@ -100,15 +58,13 @@ class ExtractQuicktime(pype.api.Extractor): preset = {} self.log.info('using viewport preset: {}'.format(capture_preset)) - #preset["off_screen"] = False - preset['camera'] = camera preset['format'] = "image" # preset['compression'] = "qt" preset['quality'] = 50 preset['compression'] = "jpg" - preset['start_frame'] = 1 - preset['end_frame'] = 25 + preset['start_frame'] = start + preset['end_frame'] = end preset['camera_options'] = { "displayGateMask": False, "displayResolution": False, @@ -143,50 +99,34 @@ class ExtractQuicktime(pype.api.Extractor): self.log.info("file list {}".format(playblast)) # self.log.info("Calculating HUD data overlay") - # stagingdir = "C:/Users/milan.kolar/AppData/Local/Temp/pyblish_tmp_ucsymm" collected_frames = os.listdir(stagingdir) collections, remainder = clique.assemble(collected_frames) - input_path = os.path.join(stagingdir, collections[0].format('{head}{padding}{tail}')) + input_path = os.path.join( + stagingdir, collections[0].format('{head}{padding}{tail}')) self.log.info("input {}".format(input_path)) movieFile = filename + ".mov" full_movie_path = os.path.join(stagingdir, movieFile) self.log.info("output {}".format(full_movie_path)) - # fls = [os.path.join(stagingdir, filename).replace("\\","/") for f in os.listdir( dir_path ) if f.endswith(preset['compression'])] - # self.log.info("file list {}}".format(fls[0])) - - out, err = ( - ffmpeg - .input(input_path, framerate=25) - .output(full_movie_path) - .run(overwrite_output=True) - ) + with avalon.maya.suspended_refresh(): + try: + ( + ffmpeg + .input(input_path, framerate=fps, start_number=int(start)) + .output(full_movie_path) + .run(overwrite_output=True, + capture_stdout=True, + capture_stderr=True) + ) + except ffmpeg.Error as e: + ffmpeg_error = 'ffmpeg error: {}'.format(e.stderr) + self.log.error(ffmpeg_error) + raise RuntimeError(ffmpeg_error) if "files" not in instance.data: instance.data["files"] = list() instance.data["files"].append(movieFile) - # ftrackStrings = fStrings.annotationData() - # nData = ftrackStrings.niceData - # nData['version'] = instance.context.data('version') - # fFrame = int(pm.playbackOptions( q = True, minTime = True)) - # eFrame = int(pm.playbackOptions( q = True, maxTime = True)) - # nData['frame'] = [(str("{0:05d}".format(f))) for f in range(fFrame, eFrame + 1)] - # soundOfst = int(float(nData['oFStart'])) - int(float(nData['handle'])) - fFrame - # soundFile = mu.giveMePublishedAudio() - # self.log.info("SOUND offset %s" % str(soundOfst)) - # self.log.info("SOUND source video to %s" % str(soundFile)) - # ann = dHUD.draftAnnotate() - # if soundFile: - # ann.addAnotation(seqFls = fls, outputMoviePth = movieFullPth, annotateDataArr = nData, soundFile = soundFile, soundOffset = soundOfst) - # else: - # ann.addAnotation(seqFls = fls, outputMoviePth = movieFullPth, annotateDataArr = nData) - - # for f in fls: - # os.remove(f) - - # playblast = (ann.expPth).replace("\\","/") - @contextlib.contextmanager def maintained_time(): diff --git a/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py b/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py index 6ade81764b..7d2c126af9 100644 --- a/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py +++ b/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py @@ -23,6 +23,7 @@ class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin): version = (0, 1, 0) label = 'Mesh Edge Length Non Zero' actions = [pype.maya.action.SelectInvalidAction] + optional = True __tolerance = 1e-5 diff --git a/pype/plugins/maya/publish/validate_render_image_rule.py b/pype/plugins/maya/publish/validate_render_image_rule.py index 9a718afc13..377dbfeadc 100644 --- a/pype/plugins/maya/publish/validate_render_image_rule.py +++ b/pype/plugins/maya/publish/validate_render_image_rule.py @@ -9,7 +9,7 @@ def get_file_rule(rule): return mel.eval('workspace -query -fileRuleEntry "{}"'.format(rule)) -class ValidateRenderImageRule(pyblish.api.ContextPlugin): +class ValidateRenderImageRule(pyblish.api.InstancePlugin): """Validates "images" file rule is set to "renders/" """ @@ -19,7 +19,7 @@ class ValidateRenderImageRule(pyblish.api.ContextPlugin): hosts = ["maya"] families = ["renderlayer"] - def process(self, context): + def process(self, instance): assert get_file_rule("images") == "renders", ( "Workspace's `images` file rule must be set to: renders"