Merge branch 'develop' into feature/PYPE-570-maya-renderlayer-creator

This commit is contained in:
Milan Kolar 2020-02-26 17:27:08 +01:00
commit 0dc5c7a21d
11 changed files with 267 additions and 57 deletions

View file

@ -13,6 +13,62 @@ import avalon
log = logging.getLogger(__name__)
def get_paths_from_environ(env_key, return_first=False):
"""Return existing paths from specific envirnment variable.
:param env_key: Environment key where should look for paths.
:type env_key: str
:param return_first: Return first path on `True`, list of all on `False`.
:type return_first: boolean
Difference when none of paths exists:
- when `return_first` is set to `False` then function returns empty list.
- when `return_first` is set to `True` then function returns `None`.
"""
existing_paths = []
paths = os.environ.get(env_key) or ""
path_items = paths.split(os.pathsep)
for path in path_items:
# Skip empty string
if not path:
continue
# Normalize path
path = os.path.normpath(path)
# Check if path exists
if os.path.exists(path):
# Return path if `return_first` is set to True
if return_first:
return path
# Store path
existing_paths.append(path)
# Return None if none of paths exists
if return_first:
return None
# Return all existing paths from environment variable
return existing_paths
def get_ffmpeg_tool_path(tool="ffmpeg"):
"""Find path to ffmpeg tool in FFMPEG_PATH paths.
Function looks for tool in paths set in FFMPEG_PATH environment. If tool
exists then returns it's full path.
Returns tool name itself when tool path was not found. (FFmpeg path may be
set in PATH environment variable)
"""
dir_paths = get_paths_from_environ("FFMPEG_PATH")
for dir_path in dir_paths:
for file_name in os.listdir(dir_path):
base, ext = os.path.splitext(file_name)
if base.lower() == tool.lower():
return os.path.join(dir_path, tool)
return tool
# Special naming case for subprocess since its a built-in method.
def _subprocess(*args, **kwargs):
"""Convenience method for getting output errors for subprocess."""

View file

@ -35,7 +35,18 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
# Find project entity
project_query = 'Project where full_name is "{0}"'.format(project_name)
self.log.debug("Project query: < {0} >".format(project_query))
project_entity = session.query(project_query).one()
project_entity = list(session.query(project_query).all())
if len(project_entity) == 0:
raise AssertionError(
"Project \"{0}\" not found in Ftrack.".format(project_name)
)
# QUESTION Is possible to happen?
elif len(project_entity) > 1:
raise AssertionError((
"Found more than one project with name \"{0}\" in Ftrack."
).format(project_name))
project_entity = project_entity[0]
self.log.debug("Project found: {0}".format(project_entity))
# Find asset entity
@ -44,7 +55,25 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
' and name is "{1}"'
).format(project_entity["id"], asset_name)
self.log.debug("Asset entity query: < {0} >".format(entity_query))
asset_entity = session.query(entity_query).one()
asset_entities = []
for entity in session.query(entity_query).all():
# Skip tasks
if entity.entity_type.lower() != "task":
asset_entities.append(entity)
if len(asset_entities) == 0:
raise AssertionError((
"Entity with name \"{0}\" not found"
" in Ftrack project \"{1}\"."
).format(asset_name, project_name))
elif len(asset_entities) > 1:
raise AssertionError((
"Found more than one entity with name \"{0}\""
" in Ftrack project \"{1}\"."
).format(asset_name, project_name))
asset_entity = asset_entities[0]
self.log.debug("Asset found: {0}".format(asset_entity))
# Find task entity if task is set
@ -53,8 +82,15 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
'Task where name is "{0}" and parent_id is "{1}"'
).format(task_name, asset_entity["id"])
self.log.debug("Task entity query: < {0} >".format(task_query))
task_entity = session.query(task_query).one()
self.log.debug("Task entity found: {0}".format(task_entity))
task_entity = session.query(task_query).first()
if not task_entity:
self.log.warning(
"Task entity with name \"{0}\" was not found.".format(
task_name
)
)
else:
self.log.debug("Task entity found: {0}".format(task_entity))
else:
task_entity = None

View file

@ -2,6 +2,7 @@ import os
import pyblish.api
import pype.api
import pype.lib
class ExtractJpegEXR(pyblish.api.InstancePlugin):
@ -56,9 +57,10 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
proj_name = os.environ.get('AVALON_PROJECT', '__default__')
profile = config_data.get(proj_name, config_data['__default__'])
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
jpeg_items = []
jpeg_items.append(
os.path.join(os.environ.get("FFMPEG_PATH"), "ffmpeg"))
jpeg_items.append(ffmpeg_path)
# override file if already exists
jpeg_items.append("-y")
# use same input args like with mov

View file

@ -2,6 +2,7 @@ import os
import pyblish.api
import clique
import pype.api
import pype.lib
class ExtractReview(pyblish.api.InstancePlugin):
@ -40,6 +41,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
# get representation and loop them
representations = inst_data["representations"]
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
# filter out mov and img sequences
representations_new = representations[:]
for repre in representations:
@ -149,6 +152,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
# necessary input data
# adds start arg only if image sequence
if isinstance(repre["files"], list):
if start_frame != repre.get("detectedStart", start_frame):
start_frame = repre.get("detectedStart")
input_args.append(
"-start_number {0} -framerate {1}".format(
start_frame, fps))
@ -324,10 +330,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
os.mkdir(stg_dir)
mov_args = [
os.path.join(
os.environ.get(
"FFMPEG_PATH",
""), "ffmpeg"),
ffmpeg_path,
" ".join(input_args),
" ".join(output_args)
]

View file

@ -1,5 +1,6 @@
import os
import pype.api
import pype.lib
import pyblish
@ -21,7 +22,7 @@ class ExtractReviewSlate(pype.api.Extractor):
suffix = "_slate"
slate_path = inst_data.get("slateFrame")
ffmpeg_path = os.path.join(os.environ.get("FFMPEG_PATH", ""), "ffmpeg")
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
to_width = 1920
to_height = 1080

View file

@ -1,13 +1,14 @@
import pyblish.api
import os
import subprocess
import pype.lib
try:
import os.errno as errno
except ImportError:
import errno
class ValidateFfmpegInstallef(pyblish.api.Validator):
class ValidateFFmpegInstalled(pyblish.api.Validator):
"""Validate availability of ffmpeg tool in PATH"""
order = pyblish.api.ValidatorOrder
@ -27,10 +28,8 @@ class ValidateFfmpegInstallef(pyblish.api.Validator):
return True
def process(self, instance):
self.log.info("ffmpeg path: `{}`".format(
os.environ.get("FFMPEG_PATH", "")))
if self.is_tool(
os.path.join(
os.environ.get("FFMPEG_PATH", ""), "ffmpeg")) is False:
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
self.log.info("ffmpeg path: `{}`".format(ffmpeg_path))
if self.is_tool(ffmpeg_path) is False:
self.log.error("ffmpeg not found in PATH")
raise RuntimeError('ffmpeg not installed.')

View file

@ -0,0 +1,97 @@
import os
import types
import maya.cmds as cmds
import pyblish.api
import pype.api
import pype.maya.action
class ValidateAssRelativePaths(pyblish.api.InstancePlugin):
"""Ensure exporting ass file has set relative texture paths"""
order = pype.api.ValidateContentsOrder
hosts = ['maya']
families = ['ass']
label = "ASS has relative texture paths"
actions = [pype.api.RepairAction]
def process(self, instance):
# we cannot ask this until user open render settings as
# `defaultArnoldRenderOptions` doesn't exists
try:
relative_texture = cmds.getAttr(
"defaultArnoldRenderOptions.absolute_texture_paths")
relative_procedural = cmds.getAttr(
"defaultArnoldRenderOptions.absolute_procedural_paths")
texture_search_path = cmds.getAttr(
"defaultArnoldRenderOptions.tspath"
)
procedural_search_path = cmds.getAttr(
"defaultArnoldRenderOptions.pspath"
)
except ValueError:
assert False, ("Can not validate, render setting were not opened "
"yet so Arnold setting cannot be validate")
scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True))
scene_name, _ = os.path.splitext(scene_basename)
project_root = "{}{}{}".format(
os.environ.get("AVALON_PROJECTS"),
os.path.sep,
os.environ.get("AVALON_PROJECT")
)
assert self.maya_is_true(relative_texture) is not True, \
("Texture path is set to be absolute")
assert self.maya_is_true(relative_procedural) is not True, \
("Procedural path is set to be absolute")
texture_search_path = texture_search_path.replace("\\", "/")
procedural_search_path = procedural_search_path.replace("\\", "/")
project_root = project_root.replace("\\", "/")
assert project_root in texture_search_path, \
("Project root is not in texture_search_path")
assert project_root in procedural_search_path, \
("Project root is not in procedural_search_path")
@classmethod
def repair(cls, instance):
texture_search_path = cmds.getAttr(
"defaultArnoldRenderOptions.tspath"
)
procedural_search_path = cmds.getAttr(
"defaultArnoldRenderOptions.pspath"
)
project_root = "{}{}{}".format(
os.environ.get("AVALON_PROJECTS"),
os.path.sep,
os.environ.get("AVALON_PROJECT"),
).replace("\\", "/")
cmds.setAttr("defaultArnoldRenderOptions.tspath",
project_root + os.pathsep + texture_search_path,
type="string")
cmds.setAttr("defaultArnoldRenderOptions.pspath",
project_root + os.pathsep + procedural_search_path,
type="string")
cmds.setAttr("defaultArnoldRenderOptions.absolute_procedural_paths",
False)
cmds.setAttr("defaultArnoldRenderOptions.absolute_texture_paths",
False)
def maya_is_true(self, attr_val):
"""
Whether a Maya attr evaluates to True.
When querying an attribute value from an ambiguous object the
Maya API will return a list of values, which need to be properly
handled to evaluate properly.
"""
if isinstance(attr_val, types.BooleanType):
return attr_val
elif isinstance(attr_val, (types.ListType, types.GeneratorType)):
return any(attr_val)
else:
return bool(attr_val)

View file

@ -4,6 +4,7 @@ import tempfile
import pyblish.api
import clique
import pype.api
import pype.lib
class ExtractReviewSP(pyblish.api.InstancePlugin):
@ -148,12 +149,7 @@ class ExtractReviewSP(pyblish.api.InstancePlugin):
# output filename
output_args.append(full_output_path)
ffmpeg_path = os.getenv("FFMPEG_PATH", "")
if ffmpeg_path:
ffmpeg_path += "/ffmpeg"
else:
ffmpeg_path = "ffmpeg"
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
mov_args = [
ffmpeg_path,
" ".join(input_args),

View file

@ -3,6 +3,7 @@ import tempfile
import subprocess
import pyblish.api
import pype.api
import pype.lib
class ExtractThumbnailSP(pyblish.api.InstancePlugin):
@ -73,11 +74,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
config_data.get("__default__", {})
)
ffmpeg_path = os.getenv("FFMPEG_PATH", "")
if ffmpeg_path:
ffmpeg_path += "/ffmpeg"
else:
ffmpeg_path = "ffmpeg"
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
jpeg_items = []
jpeg_items.append(ffmpeg_path)

View file

@ -1,33 +1,27 @@
import os
import sys
import re
import datetime
import subprocess
import json
import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins
from pypeapp.lib import config
from pype import api as pype
from subprocess import Popen, PIPE
# FFmpeg in PATH is required
from pypeapp import Logger
import pype.lib
log = Logger().get_logger("BurninWrapper", "burninwrap")
log = pype.Logger().get_logger("BurninWrapper", "burninwrap")
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe")
ffmpeg_path = os.environ.get("FFMPEG_PATH")
if ffmpeg_path and os.path.exists(ffmpeg_path):
# add separator "/" or "\" to be prepared for next part
ffmpeg_path += os.path.sep
else:
ffmpeg_path = ""
FFMPEG = (
'{} -loglevel panic -i %(input)s %(filters)s %(args)s%(output)s'
).format(os.path.normpath(ffmpeg_path + "ffmpeg"))
).format(ffmpeg_path)
FFPROBE = (
'{} -v quiet -print_format json -show_format -show_streams %(source)s'
).format(os.path.normpath(ffmpeg_path + "ffprobe"))
).format(ffprobe_path)
DRAWTEXT = (
"drawtext=text=\\'%(text)s\\':x=%(x)s:y=%(y)s:fontcolor="
@ -41,6 +35,7 @@ TIMECODE = (
MISSING_KEY_VALUE = "N/A"
CURRENT_FRAME_KEY = "{current_frame}"
CURRENT_FRAME_SPLITTER = "_-_CURRENT_FRAME_-_"
TIME_CODE_KEY = "{timecode}"
@ -136,7 +131,9 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
if options_init:
self.options_init.update(options_init)
def add_text(self, text, align, frame_start=None, options=None):
def add_text(
self, text, align, frame_start=None, frame_end=None, options=None
):
"""
Adding static text to a filter.
@ -152,11 +149,15 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
if frame_start:
options["frame_offset"] = frame_start
# `frame_end` is only for meassurements of text position
if frame_end:
options["frame_end"] = frame_end
self._add_burnin(text, align, options, DRAWTEXT)
def add_timecode(
self, align, frame_start=None, frame_start_tc=None, text=None,
options=None
self, align, frame_start=None, frame_end=None, frame_start_tc=None,
text=None, options=None
):
"""
Convenience method to create the frame number expression.
@ -174,6 +175,10 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
if frame_start:
options["frame_offset"] = frame_start
# `frame_end` is only for meassurements of text position
if frame_end:
options["frame_end"] = frame_end
if not frame_start_tc:
frame_start_tc = options["frame_offset"]
@ -197,10 +202,31 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
:param enum align: alignment, must use provided enum flags
:param dict options:
"""
final_text = text
text_for_size = text
if CURRENT_FRAME_SPLITTER in text:
frame_start = options["frame_offset"]
frame_end = options.get("frame_end", frame_start)
if not frame_start:
replacement_final = replacement_size = str(MISSING_KEY_VALUE)
else:
replacement_final = "\\'{}\\'".format(
r'%%{eif\:n+%d\:d}' % frame_start
)
replacement_size = str(frame_end)
final_text = final_text.replace(
CURRENT_FRAME_SPLITTER, replacement_final
)
text_for_size = text_for_size.replace(
CURRENT_FRAME_SPLITTER, replacement_size
)
resolution = self.resolution
data = {
'text': (
text
final_text
.replace(",", r"\,")
.replace(':', r'\:')
),
@ -208,7 +234,7 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
'size': options['font_size']
}
timecode_text = options.get("timecode") or ""
text_for_size = text + timecode_text
text_for_size += timecode_text
data.update(options)
data.update(
ffmpeg_burnins._drawtext(align, resolution, text_for_size, options)
@ -272,7 +298,7 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
)
print(command)
proc = Popen(command, shell=True)
proc = subprocess.Popen(command, shell=True)
proc.communicate()
if proc.returncode != 0:
raise RuntimeError("Failed to render '%s': %s'"
@ -368,6 +394,7 @@ def burnins_from_data(
burnin = ModifiedBurnins(input_path, options_init=options_init)
frame_start = data.get("frame_start")
frame_end = data.get("frame_end")
frame_start_tc = data.get('frame_start_tc', frame_start)
stream = burnin._streams[0]
@ -382,7 +409,7 @@ def burnins_from_data(
# Check frame start and add expression if is available
if frame_start is not None:
data[CURRENT_FRAME_KEY[1:-1]] = r'%%{eif\:n+%d\:d}' % frame_start
data[CURRENT_FRAME_KEY[1:-1]] = CURRENT_FRAME_SPLITTER
if frame_start_tc is not None:
data[TIME_CODE_KEY[1:-1]] = TIME_CODE_KEY
@ -432,7 +459,7 @@ def burnins_from_data(
# Handle timecode differently
if has_timecode:
args = [align, frame_start, frame_start_tc]
args = [align, frame_start, frame_end, frame_start_tc]
if not value.startswith(TIME_CODE_KEY):
value_items = value.split(TIME_CODE_KEY)
text = value_items[0].format(**data)
@ -442,7 +469,7 @@ def burnins_from_data(
continue
text = value.format(**data)
burnin.add_text(text, align, frame_start)
burnin.add_text(text, align, frame_start, frame_end)
codec_args = ""
if codec_data:

View file

@ -4,6 +4,7 @@ import json
import clique
import subprocess
from pypeapp import config
import pype.lib
from . import QtWidgets, QtCore
from . import DropEmpty, ComponentsList, ComponentItem
@ -224,12 +225,7 @@ class DropDataFrame(QtWidgets.QFrame):
self._process_data(data)
def load_data_with_probe(self, filepath):
ffprobe_path = os.getenv("FFMPEG_PATH", "")
if ffprobe_path:
ffprobe_path += '/ffprobe'
else:
ffprobe_path = 'ffprobe'
ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe")
args = [
ffprobe_path,
'-v', 'quiet',