diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py
index 36affe852b..00565c5819 100644
--- a/openpype/hosts/maya/plugins/publish/collect_review.py
+++ b/openpype/hosts/maya/plugins/publish/collect_review.py
@@ -5,6 +5,7 @@ import pyblish.api
from openpype.client import get_subset_by_name
from openpype.pipeline import legacy_io
+from openpype.hosts.maya.api.lib import get_attribute_input
class CollectReview(pyblish.api.InstancePlugin):
@@ -146,3 +147,21 @@ class CollectReview(pyblish.api.InstancePlugin):
"filename": node.filename.get()
}
)
+
+ # Collect focal length.
+ attr = camera + ".focalLength"
+ focal_length = None
+ if get_attribute_input(attr):
+ start = instance.data["frameStart"]
+ end = instance.data["frameEnd"] + 1
+ focal_length = [
+ cmds.getAttr(attr, time=t) for t in range(int(start), int(end))
+ ]
+ else:
+ focal_length = cmds.getAttr(attr)
+
+ key = "focalLength"
+ try:
+ instance.data["burninDataMembers"][key] = focal_length
+ except KeyError:
+ instance.data["burninDataMembers"] = {key: focal_length}
diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py
index 7a929a0ade..6f9a095285 100644
--- a/openpype/lib/execute.py
+++ b/openpype/lib/execute.py
@@ -8,6 +8,8 @@ import tempfile
from .log import Logger
from .vendor_bin_utils import find_executable
+from .openpype_version import is_running_from_build
+
# MSDN process creation flag (Windows only)
CREATE_NO_WINDOW = 0x08000000
@@ -200,6 +202,11 @@ def run_openpype_process(*args, **kwargs):
# Skip envs that can affect OpenPype process
# - fill more if you find more
env = clean_envs_for_openpype_process(os.environ)
+
+ # Only keep OpenPype version if we are running from build.
+ if not is_running_from_build():
+ env.pop("OPENPYPE_VERSION", None)
+
return run_subprocess(args, env=env, **kwargs)
diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py
index 44ba4a5025..95575444b2 100644
--- a/openpype/plugins/publish/extract_burnin.py
+++ b/openpype/plugins/publish/extract_burnin.py
@@ -252,6 +252,9 @@ class ExtractBurnin(publish.Extractor):
# Add context data burnin_data.
burnin_data["custom"] = custom_data
+ # Add data members.
+ burnin_data.update(instance.data.get("burninDataMembers", {}))
+
# Add source camera name to burnin data
camera_name = repre.get("camera_name")
if camera_name:
diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py
index ef449f4f74..d0a4266941 100644
--- a/openpype/scripts/otio_burnin.py
+++ b/openpype/scripts/otio_burnin.py
@@ -4,8 +4,10 @@ import re
import subprocess
import platform
import json
-import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins
+import tempfile
+from string import Formatter
+import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins
from openpype.lib import (
get_ffmpeg_tool_path,
get_ffmpeg_codec_args,
@@ -23,7 +25,7 @@ FFMPEG = (
).format(ffmpeg_path)
DRAWTEXT = (
- "drawtext=fontfile='%(font)s':text=\\'%(text)s\\':"
+ "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':"
"x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d"
)
TIMECODE = (
@@ -39,6 +41,45 @@ TIMECODE_KEY = "{timecode}"
SOURCE_TIMECODE_KEY = "{source_timecode}"
+def convert_list_to_command(list_to_convert, fps, label=""):
+ """Convert a list of values to a drawtext command file for ffmpeg `sendcmd`
+
+ The list of values is expected to have a value per frame. If the video
+ file ends up being longer than the amount of samples per frame than the
+ last value will be held.
+
+ Args:
+ list_to_convert (list): List of values per frame.
+ fps (float or int): The expected frame per seconds of the output file.
+ label (str): Label for the drawtext, if specific drawtext filter is
+ required
+
+ Returns:
+ str: Filepath to the temporary drawtext command file.
+
+ """
+
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
+ for i, value in enumerate(list_to_convert):
+ seconds = i / fps
+
+ # Escape special character
+ value = str(value).replace(":", "\\:")
+
+ filter = "drawtext"
+ if label:
+ filter += "@" + label
+
+ line = (
+ "{start} {filter} reinit text='{value}';"
+ "\n".format(start=seconds, filter=filter, value=value)
+ )
+
+ f.write(line)
+ f.flush()
+ return f.name
+
+
def _get_ffprobe_data(source):
"""Reimplemented from otio burnins to be able use full path to ffprobe
:param str source: source media file
@@ -144,7 +185,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
self.options_init.update(options_init)
def add_text(
- self, text, align, frame_start=None, frame_end=None, options=None
+ self,
+ text,
+ align,
+ frame_start=None,
+ frame_end=None,
+ options=None,
+ cmd=""
):
"""
Adding static text to a filter.
@@ -165,7 +212,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
if frame_end is not None:
options["frame_end"] = frame_end
- self._add_burnin(text, align, options, DRAWTEXT)
+ draw_text = DRAWTEXT
+ if cmd:
+ draw_text = "{}, {}".format(cmd, DRAWTEXT)
+
+ options["label"] = align
+
+ self._add_burnin(text, align, options, draw_text)
def add_timecode(
self, align, frame_start=None, frame_end=None, frame_start_tc=None,
@@ -408,11 +461,13 @@ def burnins_from_data(
True by default.
Presets must be set separately. Should be dict with 2 keys:
- - "options" - sets look of burnins - colors, opacity,...(more info: ModifiedBurnins doc)
+ - "options" - sets look of burnins - colors, opacity,...
+ (more info: ModifiedBurnins doc)
- *OPTIONAL* default values are used when not included
- "burnins" - contains dictionary with burnins settings
- *OPTIONAL* burnins won't be added (easier is not to use this)
- - each key of "burnins" represents Alignment, there are 6 possibilities:
+ - each key of "burnins" represents Alignment,
+ there are 6 possibilities:
TOP_LEFT TOP_CENTERED TOP_RIGHT
BOTTOM_LEFT BOTTOM_CENTERED BOTTOM_RIGHT
- value must be string with text you want to burn-in
@@ -491,13 +546,14 @@ def burnins_from_data(
if source_timecode is not None:
data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY
+ clean_up_paths = []
for align_text, value in burnin_values.items():
if not value:
continue
- if isinstance(value, (dict, list, tuple)):
+ if isinstance(value, dict):
raise TypeError((
- "Expected string or number type."
+ "Expected string, number or list type."
" Got: {} - \"{}\""
" (Make sure you have new burnin presets)."
).format(str(type(value)), str(value)))
@@ -533,8 +589,48 @@ def burnins_from_data(
print("Source does not have set timecode value.")
value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE)
- key_pattern = re.compile(r"(\{.*?[^{0]*\})")
+ # Convert lists.
+ cmd = ""
+ text = None
+ keys = [i[1] for i in Formatter().parse(value) if i[1] is not None]
+ list_to_convert = []
+ # Warn about nested dictionary support for lists. Ei. we dont support
+ # it.
+ if "[" in "".join(keys):
+ print(
+ "We dont support converting nested dictionaries to lists,"
+ " so skipping {}".format(value)
+ )
+ else:
+ for key in keys:
+ data_value = data[key]
+
+ # Multiple lists are not supported.
+ if isinstance(data_value, list) and list_to_convert:
+ raise ValueError(
+ "Found multiple lists to convert, which is not "
+ "supported: {}".format(value)
+ )
+
+ if isinstance(data_value, list):
+ print("Found list to convert: {}".format(data_value))
+ for v in data_value:
+ data[key] = v
+ list_to_convert.append(value.format(**data))
+
+ if list_to_convert:
+ value = list_to_convert[0]
+ path = convert_list_to_command(
+ list_to_convert, data["fps"], label=align
+ )
+ cmd = "sendcmd=f='{}'".format(path)
+ cmd = cmd.replace("\\", "/")
+ cmd = cmd.replace(":", "\\:")
+ clean_up_paths.append(path)
+
+ # Failsafe for missing keys.
+ key_pattern = re.compile(r"(\{.*?[^{0]*\})")
missing_keys = []
for group in key_pattern.findall(value):
try:
@@ -568,7 +664,8 @@ def burnins_from_data(
continue
text = value.format(**data)
- burnin.add_text(text, align, frame_start, frame_end)
+
+ burnin.add_text(text, align, frame_start, frame_end, cmd=cmd)
ffmpeg_args = []
if codec_data:
@@ -599,6 +696,8 @@ def burnins_from_data(
burnin.render(
output_path, args=ffmpeg_args_str, overwrite=overwrite, **data
)
+ for path in clean_up_paths:
+ os.remove(path)
if __name__ == "__main__":
diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json
index aba840da78..30e56300d1 100644
--- a/openpype/settings/defaults/project_settings/global.json
+++ b/openpype/settings/defaults/project_settings/global.json
@@ -249,6 +249,29 @@
}
}
}
+ },
+ {
+ "families": [],
+ "hosts": [
+ "maya"
+ ],
+ "task_types": [],
+ "task_names": [],
+ "subsets": [],
+ "burnins": {
+ "maya_burnin": {
+ "TOP_LEFT": "{yy}-{mm}-{dd}",
+ "TOP_CENTERED": "{focalLength:.2f} mm",
+ "TOP_RIGHT": "{anatomy[version]}",
+ "BOTTOM_LEFT": "{username}",
+ "BOTTOM_CENTERED": "{asset}",
+ "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}",
+ "filter": {
+ "families": [],
+ "tags": []
+ }
+ }
+ }
}
]
},
diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md
index e589f7d14b..44c2a28dec 100644
--- a/website/docs/pype2/admin_presets_plugins.md
+++ b/website/docs/pype2/admin_presets_plugins.md
@@ -293,6 +293,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"**
- It is allowed to use [Anatomy templates](admin_config#anatomy) themselves in burnins if they can be filled with available data.
- Additional keys in burnins:
+
| Burnin key | Description |
| --- | --- |
| frame_start | First frame number. |
@@ -303,6 +304,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"**
| resolution_height | Resolution height. |
| fps | Fps of an output. |
| timecode | Timecode by frame start and fps. |
+ | focalLength | **Only available in Maya**
Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
:::warning
`timecode` is specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`)