mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into bugfix/OP-4096_Nuke-open_file-function-wouldnt-open-autosave-file
This commit is contained in:
commit
3a1665a8aa
6 changed files with 268 additions and 134 deletions
|
|
@ -105,7 +105,8 @@ class ImportMayaLoader(load.LoaderPlugin):
|
|||
"camera",
|
||||
"rig",
|
||||
"camerarig",
|
||||
"staticMesh"
|
||||
"staticMesh",
|
||||
"workfile"
|
||||
]
|
||||
|
||||
label = "Import"
|
||||
|
|
|
|||
|
|
@ -6,23 +6,29 @@ import maya.cmds as cmds
|
|||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import (
|
||||
load,
|
||||
legacy_io,
|
||||
get_representation_path
|
||||
)
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
unique_namespace, get_attribute_input, maintained_selection
|
||||
unique_namespace,
|
||||
get_attribute_input,
|
||||
maintained_selection,
|
||||
convert_to_maya_fps
|
||||
)
|
||||
from openpype.hosts.maya.api.pipeline import containerise
|
||||
|
||||
|
||||
def is_sequence(files):
|
||||
sequence = False
|
||||
collections, remainder = clique.assemble(files)
|
||||
collections, remainder = clique.assemble(files, minimum_items=1)
|
||||
if collections:
|
||||
sequence = True
|
||||
|
||||
return sequence
|
||||
|
||||
|
||||
def get_current_session_fps():
|
||||
session_fps = float(legacy_io.Session.get('AVALON_FPS', 25))
|
||||
return convert_to_maya_fps(session_fps)
|
||||
|
||||
class ArnoldStandinLoader(load.LoaderPlugin):
|
||||
"""Load as Arnold standin"""
|
||||
|
||||
|
|
@ -90,6 +96,9 @@ class ArnoldStandinLoader(load.LoaderPlugin):
|
|||
sequence = is_sequence(os.listdir(os.path.dirname(self.fname)))
|
||||
cmds.setAttr(standin_shape + ".useFrameExtension", sequence)
|
||||
|
||||
fps = float(version["data"].get("fps"))or get_current_session_fps()
|
||||
cmds.setAttr(standin_shape + ".abcFPS", fps)
|
||||
|
||||
nodes = [root, standin, standin_shape]
|
||||
if operator is not None:
|
||||
nodes.append(operator)
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ class BatchPublishEndpoint(WebpublishApiEndpoint):
|
|||
log.debug("Adding to queue")
|
||||
self.resource.studio_task_queue.append(args)
|
||||
else:
|
||||
subprocess.call(args)
|
||||
subprocess.Popen(args)
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
|
|
|
|||
|
|
@ -890,7 +890,7 @@ class SyncEntitiesFactory:
|
|||
else:
|
||||
parent_dict = self.entities_dict.get(parent_id, {})
|
||||
|
||||
for child_id in parent_dict.get("children", []):
|
||||
for child_id in list(parent_dict.get("children", [])):
|
||||
# keep original `remove` value for all children
|
||||
_remove = (remove is True)
|
||||
if not _remove:
|
||||
|
|
|
|||
|
|
@ -266,6 +266,16 @@ class ExtractBurnin(publish.Extractor):
|
|||
first_output = True
|
||||
|
||||
files_to_delete = []
|
||||
|
||||
repre_burnin_options = copy.deepcopy(burnin_options)
|
||||
# Use fps from representation for output in options
|
||||
fps = repre.get("fps")
|
||||
if fps is not None:
|
||||
repre_burnin_options["fps"] = fps
|
||||
# TODO Should we use fps from source representation to fill
|
||||
# it in review?
|
||||
# burnin_data["fps"] = fps
|
||||
|
||||
for filename_suffix, burnin_def in repre_burnin_defs.items():
|
||||
new_repre = copy.deepcopy(repre)
|
||||
new_repre["stagingDir"] = src_repre_staging_dir
|
||||
|
|
@ -308,7 +318,7 @@ class ExtractBurnin(publish.Extractor):
|
|||
"input": temp_data["full_input_path"],
|
||||
"output": temp_data["full_output_path"],
|
||||
"burnin_data": burnin_data,
|
||||
"options": copy.deepcopy(burnin_options),
|
||||
"options": repre_burnin_options,
|
||||
"values": burnin_values,
|
||||
"full_input_path": temp_data["full_input_paths"][0],
|
||||
"first_frame": temp_data["first_frame"],
|
||||
|
|
@ -463,15 +473,11 @@ class ExtractBurnin(publish.Extractor):
|
|||
|
||||
handle_start = instance.data.get("handleStart")
|
||||
if handle_start is None:
|
||||
handle_start = context.data.get("handleStart")
|
||||
if handle_start is None:
|
||||
handle_start = handles
|
||||
handle_start = context.data.get("handleStart") or 0
|
||||
|
||||
handle_end = instance.data.get("handleEnd")
|
||||
if handle_end is None:
|
||||
handle_end = context.data.get("handleEnd")
|
||||
if handle_end is None:
|
||||
handle_end = handles
|
||||
handle_end = context.data.get("handleEnd") or 0
|
||||
|
||||
frame_start_handle = frame_start - handle_start
|
||||
frame_end_handle = frame_end + handle_end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
import platform
|
||||
import json
|
||||
|
|
@ -13,6 +12,7 @@ from openpype.lib import (
|
|||
get_ffmpeg_codec_args,
|
||||
get_ffmpeg_format_args,
|
||||
convert_ffprobe_fps_value,
|
||||
convert_ffprobe_fps_to_float,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -41,45 +41,6 @@ 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
|
||||
|
|
@ -178,6 +139,7 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
self.ffprobe_data = ffprobe_data
|
||||
self.first_frame = first_frame
|
||||
self.input_args = []
|
||||
self.cleanup_paths = []
|
||||
|
||||
super().__init__(source, source_streams)
|
||||
|
||||
|
|
@ -191,7 +153,6 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
frame_start=None,
|
||||
frame_end=None,
|
||||
options=None,
|
||||
cmd=""
|
||||
):
|
||||
"""
|
||||
Adding static text to a filter.
|
||||
|
|
@ -212,13 +173,9 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
if frame_end is not None:
|
||||
options["frame_end"] = frame_end
|
||||
|
||||
draw_text = DRAWTEXT
|
||||
if cmd:
|
||||
draw_text = "{}, {}".format(cmd, DRAWTEXT)
|
||||
|
||||
options["label"] = align
|
||||
|
||||
self._add_burnin(text, align, options, draw_text)
|
||||
self._add_burnin(text, align, options, DRAWTEXT)
|
||||
|
||||
def add_timecode(
|
||||
self, align, frame_start=None, frame_end=None, frame_start_tc=None,
|
||||
|
|
@ -263,6 +220,139 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
|
||||
self._add_burnin(text, align, options, TIMECODE)
|
||||
|
||||
def add_per_frame_text(
|
||||
self,
|
||||
text,
|
||||
align,
|
||||
frame_start,
|
||||
frame_end,
|
||||
listed_keys,
|
||||
options=None
|
||||
):
|
||||
"""Add text that changes per frame.
|
||||
|
||||
Args:
|
||||
text (str): Template string with unfilled keys that are changed
|
||||
per frame.
|
||||
align (str): Alignment of text.
|
||||
frame_start (int): Starting frame for burnins current frame.
|
||||
frame_end (int): Ending frame for burnins current frame.
|
||||
listed_keys (list): List of keys that are changed per frame.
|
||||
options (Optional[dict]): Options to affect style of burnin.
|
||||
"""
|
||||
|
||||
if not options:
|
||||
options = ffmpeg_burnins.TimeCodeOptions(**self.options_init)
|
||||
|
||||
options = options.copy()
|
||||
if frame_start is None:
|
||||
frame_start = options["frame_offset"]
|
||||
|
||||
# `frame_end` is only for meassurements of text position
|
||||
if frame_end is None:
|
||||
frame_end = options["frame_end"]
|
||||
|
||||
fps = options.get("fps")
|
||||
if not fps:
|
||||
fps = self.frame_rate
|
||||
|
||||
text_for_size = text
|
||||
if CURRENT_FRAME_SPLITTER in text:
|
||||
expr = self._get_current_frame_expression(frame_start, frame_end)
|
||||
if expr is None:
|
||||
expr = MISSING_KEY_VALUE
|
||||
text_for_size = text_for_size.replace(
|
||||
CURRENT_FRAME_SPLITTER, MISSING_KEY_VALUE)
|
||||
text = text.replace(CURRENT_FRAME_SPLITTER, expr)
|
||||
|
||||
# Find longest list with values
|
||||
longest_list_len = max(
|
||||
len(item["values"]) for item in listed_keys.values()
|
||||
)
|
||||
# Where to store formatted values per frame by key
|
||||
new_listed_keys = [{} for _ in range(longest_list_len)]
|
||||
# Find the longest value per fill key.
|
||||
# The longest value is used to determine size of burnin box.
|
||||
longest_value_by_key = {}
|
||||
for key, item in listed_keys.items():
|
||||
values = item["values"]
|
||||
# Fill the missing values from the longest list with the last
|
||||
# value to make sure all values have same "frame count"
|
||||
last_value = values[-1] if values else ""
|
||||
for _ in range(longest_list_len - len(values)):
|
||||
values.append(last_value)
|
||||
|
||||
# Prepare dictionary structure for nestes values
|
||||
# - last key is overriden on each frame loop
|
||||
item_keys = list(item["keys"])
|
||||
fill_data = {}
|
||||
sub_value = fill_data
|
||||
last_item_key = item_keys.pop(-1)
|
||||
for item_key in item_keys:
|
||||
sub_value[item_key] = {}
|
||||
sub_value = sub_value[item_key]
|
||||
|
||||
# Fill value per frame
|
||||
key_max_len = 0
|
||||
key_max_value = ""
|
||||
for value, new_values in zip(values, new_listed_keys):
|
||||
sub_value[last_item_key] = value
|
||||
try:
|
||||
value = key.format(**sub_value)
|
||||
except (TypeError, KeyError, ValueError):
|
||||
value = MISSING_KEY_VALUE
|
||||
new_values[key] = value
|
||||
|
||||
value_len = len(value)
|
||||
if value_len > key_max_len:
|
||||
key_max_value = value
|
||||
key_max_len = value_len
|
||||
|
||||
# Store the longest value
|
||||
longest_value_by_key[key] = key_max_value
|
||||
|
||||
# Make sure the longest value of each key is replaced for text size
|
||||
# calculation
|
||||
for key, value in longest_value_by_key.items():
|
||||
text_for_size = text_for_size.replace(key, value)
|
||||
|
||||
# Create temp file with instructions for each frame of text
|
||||
lines = []
|
||||
for frame, value in enumerate(new_listed_keys):
|
||||
seconds = float(frame) / fps
|
||||
# Escape special character
|
||||
new_text = text
|
||||
for _key, _value in value.items():
|
||||
_value = str(_value)
|
||||
new_text = new_text.replace(_key, str(_value))
|
||||
|
||||
new_text = (
|
||||
str(new_text)
|
||||
.replace("\\", "\\\\")
|
||||
.replace(",", "\\,")
|
||||
.replace(":", "\\:")
|
||||
)
|
||||
lines.append(
|
||||
f"{seconds} drawtext@{align} reinit text='{new_text}';")
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp:
|
||||
path = temp.name
|
||||
temp.write("\n".join(lines))
|
||||
|
||||
self.cleanup_paths.append(path)
|
||||
self.filters["drawtext"].append("sendcmd=f='{}'".format(
|
||||
path.replace("\\", "/").replace(":", "\\:")
|
||||
))
|
||||
self.add_text(text_for_size, align, frame_start, frame_end, options)
|
||||
|
||||
def _get_current_frame_expression(self, frame_start, frame_end):
|
||||
if frame_start is None:
|
||||
return None
|
||||
return (
|
||||
"%{eif:n+" + str(frame_start)
|
||||
+ ":d:" + str(len(str(frame_end))) + "}"
|
||||
)
|
||||
|
||||
def _add_burnin(self, text, align, options, draw):
|
||||
"""
|
||||
Generic method for building the filter flags.
|
||||
|
|
@ -276,18 +366,19 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
if CURRENT_FRAME_SPLITTER in text:
|
||||
frame_start = options["frame_offset"]
|
||||
frame_end = options.get("frame_end", frame_start)
|
||||
if frame_start is None:
|
||||
replacement_final = replacement_size = str(MISSING_KEY_VALUE)
|
||||
expr = self._get_current_frame_expression(frame_start, frame_end)
|
||||
if expr is not None:
|
||||
max_length = len(str(frame_end))
|
||||
# Use number '8' length times for replacement
|
||||
size_replacement = max_length * "8"
|
||||
else:
|
||||
replacement_final = "%{eif:n+" + str(frame_start) + ":d:" + \
|
||||
str(len(str(frame_end))) + "}"
|
||||
replacement_size = str(frame_end)
|
||||
expr = size_replacement = MISSING_KEY_VALUE
|
||||
|
||||
final_text = final_text.replace(
|
||||
CURRENT_FRAME_SPLITTER, replacement_final
|
||||
CURRENT_FRAME_SPLITTER, expr
|
||||
)
|
||||
text_for_size = text_for_size.replace(
|
||||
CURRENT_FRAME_SPLITTER, replacement_size
|
||||
CURRENT_FRAME_SPLITTER, size_replacement
|
||||
)
|
||||
|
||||
resolution = self.resolution
|
||||
|
|
@ -314,13 +405,11 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
ffmpeg_burnins._drawtext(align, resolution, text_for_size, options)
|
||||
)
|
||||
|
||||
arg_font_path = font_path
|
||||
if platform.system().lower() == "windows":
|
||||
arg_font_path = (
|
||||
arg_font_path
|
||||
.replace(os.sep, r'\\' + os.sep)
|
||||
.replace(':', r'\:')
|
||||
)
|
||||
arg_font_path = (
|
||||
font_path
|
||||
.replace("\\", "\\\\")
|
||||
.replace(':', r'\:')
|
||||
)
|
||||
data["font"] = arg_font_path
|
||||
|
||||
self.filters['drawtext'].append(draw % data)
|
||||
|
|
@ -347,9 +436,15 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
if overwrite:
|
||||
output = '-y {}'.format(output)
|
||||
|
||||
filters = ''
|
||||
if self.filter_string:
|
||||
filters = '-vf "{}"'.format(self.filter_string)
|
||||
filters = ""
|
||||
filter_string = self.filter_string
|
||||
if filter_string:
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp:
|
||||
temp.write(filter_string)
|
||||
filters_path = temp.name
|
||||
filters = '-filter_script "{}"'.format(filters_path)
|
||||
print("Filters:", filter_string)
|
||||
self.cleanup_paths.append(filters_path)
|
||||
|
||||
if self.first_frame is not None:
|
||||
start_number_arg = "-start_number {}".format(self.first_frame)
|
||||
|
|
@ -420,6 +515,10 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
"Failed to generate this f*cking file '%s'" % output
|
||||
)
|
||||
|
||||
for path in self.cleanup_paths:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def example(input_path, output_path):
|
||||
options_init = {
|
||||
|
|
@ -440,6 +539,51 @@ def example(input_path, output_path):
|
|||
burnin.render(output_path, overwrite=True)
|
||||
|
||||
|
||||
def prepare_fill_values(burnin_template, data):
|
||||
"""Prepare values that will be filled instead of burnin template.
|
||||
|
||||
Args:
|
||||
burnin_template (str): Burnin template string.
|
||||
data (dict[str, Any]): Data that will be used to fill template.
|
||||
|
||||
Returns:
|
||||
tuple[dict[str, dict[str, Any]], dict[str, Any], set[str]]: Filled
|
||||
values that can be used as are, listed values that have different
|
||||
value per frame and missing keys that are not present in data.
|
||||
"""
|
||||
|
||||
fill_values = {}
|
||||
listed_keys = {}
|
||||
missing_keys = set()
|
||||
for item in Formatter().parse(burnin_template):
|
||||
_, field_name, format_spec, conversion = item
|
||||
if not field_name:
|
||||
continue
|
||||
# Calculate nested keys '{project[name]}' -> ['project', 'name']
|
||||
keys = [key.rstrip("]") for key in field_name.split("[")]
|
||||
# Calculate original full key for replacement
|
||||
conversion = "!{}".format(conversion) if conversion else ""
|
||||
format_spec = ":{}".format(format_spec) if format_spec else ""
|
||||
orig_key = "{{{}{}{}}}".format(
|
||||
field_name, conversion, format_spec)
|
||||
|
||||
key_value = data
|
||||
try:
|
||||
for key in keys:
|
||||
key_value = key_value[key]
|
||||
|
||||
if isinstance(key_value, list):
|
||||
listed_keys[orig_key] = {
|
||||
"values": key_value,
|
||||
"keys": keys}
|
||||
else:
|
||||
fill_values[orig_key] = orig_key.format(**data)
|
||||
except (KeyError, TypeError):
|
||||
missing_keys.add(orig_key)
|
||||
continue
|
||||
return fill_values, listed_keys, missing_keys
|
||||
|
||||
|
||||
def burnins_from_data(
|
||||
input_path, output_path, data,
|
||||
codec_data=None, options=None, burnin_values=None, overwrite=True,
|
||||
|
|
@ -512,17 +656,26 @@ def burnins_from_data(
|
|||
frame_end = data.get("frame_end")
|
||||
frame_start_tc = data.get('frame_start_tc', frame_start)
|
||||
|
||||
stream = burnin._streams[0]
|
||||
video_stream = None
|
||||
for stream in burnin._streams:
|
||||
if stream.get("codec_type") == "video":
|
||||
video_stream = stream
|
||||
break
|
||||
|
||||
if video_stream is None:
|
||||
raise ValueError("Source didn't have video stream.")
|
||||
|
||||
if "resolution_width" not in data:
|
||||
data["resolution_width"] = stream.get("width", MISSING_KEY_VALUE)
|
||||
data["resolution_width"] = video_stream.get(
|
||||
"width", MISSING_KEY_VALUE)
|
||||
|
||||
if "resolution_height" not in data:
|
||||
data["resolution_height"] = stream.get("height", MISSING_KEY_VALUE)
|
||||
data["resolution_height"] = video_stream.get(
|
||||
"height", MISSING_KEY_VALUE)
|
||||
|
||||
r_frame_rate = video_stream.get("r_frame_rate", "0/0")
|
||||
if "fps" not in data:
|
||||
data["fps"] = convert_ffprobe_fps_value(
|
||||
stream.get("r_frame_rate", "0/0")
|
||||
)
|
||||
data["fps"] = convert_ffprobe_fps_value(r_frame_rate)
|
||||
|
||||
# Check frame start and add expression if is available
|
||||
if frame_start is not None:
|
||||
|
|
@ -531,9 +684,9 @@ def burnins_from_data(
|
|||
if frame_start_tc is not None:
|
||||
data[TIMECODE_KEY[1:-1]] = TIMECODE_KEY
|
||||
|
||||
source_timecode = stream.get("timecode")
|
||||
source_timecode = video_stream.get("timecode")
|
||||
if source_timecode is None:
|
||||
source_timecode = stream.get("tags", {}).get("timecode")
|
||||
source_timecode = video_stream.get("tags", {}).get("timecode")
|
||||
|
||||
# Use "format" key from ffprobe data
|
||||
# - this is used e.g. in mxf extension
|
||||
|
|
@ -589,59 +742,24 @@ def burnins_from_data(
|
|||
print("Source does not have set timecode value.")
|
||||
value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE)
|
||||
|
||||
# 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:
|
||||
group.format(**data)
|
||||
except (TypeError, KeyError):
|
||||
missing_keys.append(group)
|
||||
fill_values, listed_keys, missing_keys = prepare_fill_values(
|
||||
value, data
|
||||
)
|
||||
|
||||
missing_keys = list(set(missing_keys))
|
||||
for key in missing_keys:
|
||||
value = value.replace(key, MISSING_KEY_VALUE)
|
||||
|
||||
if listed_keys:
|
||||
for key, key_value in fill_values.items():
|
||||
if key == CURRENT_FRAME_KEY:
|
||||
key_value = CURRENT_FRAME_SPLITTER
|
||||
value = value.replace(key, str(key_value))
|
||||
burnin.add_per_frame_text(
|
||||
value, align, frame_start, frame_end, listed_keys
|
||||
)
|
||||
continue
|
||||
|
||||
# Handle timecode differently
|
||||
if has_source_timecode:
|
||||
args = [align, frame_start, frame_end, source_timecode]
|
||||
|
|
@ -665,7 +783,7 @@ def burnins_from_data(
|
|||
|
||||
text = value.format(**data)
|
||||
|
||||
burnin.add_text(text, align, frame_start, frame_end, cmd=cmd)
|
||||
burnin.add_text(text, align, frame_start, frame_end)
|
||||
|
||||
ffmpeg_args = []
|
||||
if codec_data:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue