Skip to content

Commit

Permalink
added pre- / post-transcribe hooks to facilitate media essence embedding
Browse files Browse the repository at this point in the history
Signed-off-by: Tim Lehr <[email protected]>
  • Loading branch information
timlehr committed Mar 14, 2024
1 parent 00ca601 commit 1d71c1b
Show file tree
Hide file tree
Showing 12 changed files with 634 additions and 66 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
install_requires=[
"OpenTimelineIO>=0.14.1",
"pyaaf2>=1.4.0",
"pyaaf2>=1.7.0",
],
extras_require={
"dev": [
Expand Down
264 changes: 234 additions & 30 deletions src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions src/otio_aaf_adapter/adapters/aaf_adapter/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the OpenTimelineIO project

import aaf2
import opentimelineio as otio


# Plugin custom hook names
HOOK_PRE_TRANSCRIBE = "otio_aaf_pre_transcribe"
HOOK_POST_TRANSCRIBE = "otio_aaf_post_transcribe"

def run_pre_transcribe_hook(
timeline: otio.schema.Timeline,
write_filepath: str,
aaf_handle: aaf2.file.AAFFile,
embed_essence: bool,
extra_kwargs: dict
) -> otio.schema.Timeline:
"""This hook runs on write, just before the timeline gets translated to pyaaf2
data."""
if HOOK_PRE_TRANSCRIBE in otio.hooks.names():
extra_kwargs.update({
"write_filepath": write_filepath,
"aaf_handle": aaf_handle,
"embed_essence": embed_essence,
})
return otio.hooks.run(HOOK_PRE_TRANSCRIBE, timeline, extra_kwargs)
return timeline


def run_post_transcribe_hook(
timeline: otio.schema.Timeline,
write_filepath: str,
aaf_handle: aaf2.file.AAFFile,
embed_essence: bool,
extra_kwargs: dict
) -> otio.schema.Timeline:
"""This hook runs on write, just after the timeline gets translated to pyaaf2
data."""
if HOOK_POST_TRANSCRIBE in otio.hooks.names():
extra_kwargs.update({
"write_filepath": write_filepath,
"aaf_handle": aaf_handle,
"embed_essence": embed_essence,
})
return otio.hooks.run(HOOK_POST_TRANSCRIBE, timeline, extra_kwargs)
return timeline
51 changes: 46 additions & 5 deletions src/otio_aaf_adapter/adapters/advanced_authoring_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import aaf2.core # noqa: E402
import aaf2.misc # noqa: E402
from otio_aaf_adapter.adapters.aaf_adapter import aaf_writer # noqa: E402
from otio_aaf_adapter.adapters.aaf_adapter import hooks


debug = False
Expand Down Expand Up @@ -1565,7 +1566,8 @@ def read_from_file(
simplify=True,
transcribe_log=False,
attach_markers=True,
bake_keyframed_properties=False
bake_keyframed_properties=False,
**kwargs
):
"""Reads AAF content from `filepath` and outputs an OTIO timeline object.
Expand Down Expand Up @@ -1619,15 +1621,40 @@ def read_from_file(
return result


def write_to_file(input_otio, filepath, **kwargs):
def write_to_file(input_otio, filepath, embed_essence=False,
create_edgecode=True, **kwargs):
"""Serialize `input_otio` to an AAF file at `filepath`.
with aaf2.open(filepath, "w") as f:
Args:
input_otio(otio.schema.Timeline): input timeline
filepath(str): output filepath
embed_essence(Optional[bool]): if `True`, media essence will be included in AAF
create_edgecode(bool): if `True` each clip will get an EdgeCode slot
assigned that defines the Avid Frame Count Start / End.
**kwargs: extra adapter arguments
"""

timeline = aaf_writer._stackify_nested_groups(input_otio)
with aaf2.open(filepath, "w") as f:
print(f"ADAPTER KWARGS: {kwargs}")
# trigger adapter specific pre-transcribe write hook
hook_tl = hooks.run_pre_transcribe_hook(timeline=input_otio,
write_filepath=filepath,
aaf_handle=f,
embed_essence=embed_essence,
extra_kwargs=kwargs.get(
"hook_function_argument_map", {}
))

timeline = aaf_writer._stackify_nested_groups(hook_tl)

aaf_writer.validate_metadata(timeline)

otio2aaf = aaf_writer.AAFFileTranscriber(timeline, f, **kwargs)
otio2aaf = aaf_writer.AAFFileTranscriber(input_otio=timeline,
aaf_file=f,
embed_essence=embed_essence,
create_edgecode=create_edgecode,
**kwargs)

if not isinstance(timeline, otio.schema.Timeline):
raise otio.exceptions.NotSupportedError(
Expand All @@ -1644,3 +1671,17 @@ def write_to_file(input_otio, filepath, **kwargs):
result = transcriber.transcribe(otio_child)
if result:
transcriber.sequence.components.append(result)

# trigger adapter specific post-transcribe write hook
hooks.run_post_transcribe_hook(timeline=timeline,
write_filepath=filepath,
aaf_handle=f,
embed_essence=embed_essence,
extra_kwargs=kwargs.get(
"hook_function_argument_map", {}
))


def adapter_hook_names():
"""Returns names of custom hooks implemented by this adapter."""
return ["otio_aaf_pre_transcribe", "otio_aaf_post_transcribe"]
6 changes: 5 additions & 1 deletion src/otio_aaf_adapter/plugin_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
"filepath" : "adapters/advanced_authoring_format.py",
"suffixes" : ["aaf"]
}
]
],
"hooks" : {
"otio_aaf_pre_transcribe": [],
"otio_aaf_post_transcribe": []
}
}
21 changes: 21 additions & 0 deletions tests/hooks_plugin_example/plugin_manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"OTIO_SCHEMA" : "PluginManifest.1",
"hook_scripts" : [
{
"OTIO_SCHEMA" : "HookScript.1",
"name" : "pre_aaf_transcribe_hook",
"execution_scope" : "in process",
"filepath" : "pre_aaf_transcribe_hook.py"
},
{
"OTIO_SCHEMA" : "HookScript.1",
"name" : "post_aaf_transcribe_hook",
"execution_scope" : "in process",
"filepath" : "post_aaf_transcribe_hook.py"
}
],
"hooks" : {
"otio_aaf_pre_transcribe": ["pre_aaf_transcribe_hook"],
"otio_aaf_post_transcribe": ["post_aaf_transcribe_hook"]
}
}
21 changes: 21 additions & 0 deletions tests/hooks_plugin_example/post_aaf_transcribe_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Example hook that runs post-transcription on a write operation.
This hook is useful to clean up temporary files or metadata post-write.
"""
from otio_aaf_adapter.adapters.aaf_adapter.aaf_writer import AAFAdapterError


def hook_function(in_timeline, argument_map=None):
if argument_map.get("test_post_hook_raise", False):
raise AAFAdapterError()

if not argument_map.get("embed_essence", False):
# no essence embedding requested, skip the hook
return in_timeline

for clip in in_timeline.find_clips():
# reset target URL to pre-conversion media, remove metadata
original_url = clip.media_reference.metadata.pop("original_target_url")
if original_url:
clip.media_reference.target_url = original_url

return in_timeline
27 changes: 27 additions & 0 deletions tests/hooks_plugin_example/pre_aaf_transcribe_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Example hook that runs pre-transcription on a write operation.
This can be useful for just-in-time transcoding of media references to DNX data /
WAVE audio files.
"""
import os
from pathlib import Path
from otio_aaf_adapter.adapters.aaf_adapter.aaf_writer import AAFAdapterError


def hook_function(in_timeline, argument_map=None):
if argument_map.get("test_pre_hook_raise", False):
raise AAFAdapterError()

if not argument_map.get("embed_essence", False):
# no essence embedding requested, skip the hook
return in_timeline

for clip in in_timeline.find_clips():
# mock convert video media references, this could be done with ffmpeg
if Path(clip.media_reference.target_url).suffix == ".mov":
converted_url = Path(clip.media_reference.target_url).with_suffix(".dnx")
clip.media_reference.metadata[
"original_target_url"
] = clip.media_reference.target_url
clip.media_reference.target_url = os.fspath(converted_url)

return in_timeline
Binary file added tests/sample_data/picchu_sample.wav
Binary file not shown.
Binary file added tests/sample_data/picchu_seq0100_snippet_dnx.dnx
Binary file not shown.
Binary file added tests/sample_data/picchu_seq0100_snippet_dnx.mov
Binary file not shown.
Loading

0 comments on commit 1d71c1b

Please sign in to comment.