Fix multiple review clips in OTIO review plugins with tests.

This commit is contained in:
robin 2024-10-02 11:06:29 -04:00
parent 7bd382a187
commit 41302936c2
4 changed files with 1969 additions and 45 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,289 @@
{
"OTIO_SCHEMA": "Track.1",
"metadata": {},
"name": "Video 2",
"source_range": null,
"effects": [],
"markers": [],
"enabled": true,
"children": [
{
"OTIO_SCHEMA": "Gap.1",
"metadata": {},
"name": "",
"source_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 2.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 0.0
}
},
"effects": [],
"markers": [],
"enabled": true
},
{
"OTIO_SCHEMA": "Clip.2",
"metadata": {},
"name": "output",
"source_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 88.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 0.0
}
},
"effects": [],
"markers": [],
"enabled": true,
"media_references": {
"DEFAULT_MEDIA": {
"OTIO_SCHEMA": "ImageSequenceReference.1",
"metadata": {
"clip.properties.blendfunc": "0",
"clip.properties.colourspacename": "default ()",
"clip.properties.domainroot": "",
"clip.properties.enabled": "1",
"clip.properties.expanded": "1",
"clip.properties.opacity": "1",
"clip.properties.valuesource": "",
"foundry.source.audio": "",
"foundry.source.bitmapsize": "0",
"foundry.source.bitsperchannel": "0",
"foundry.source.channelformat": "integer",
"foundry.source.colourtransform": "scene_linear",
"foundry.source.duration": "101",
"foundry.source.filename": "output.%04d.exr 1000-1100",
"foundry.source.filesize": "",
"foundry.source.fragments": "101",
"foundry.source.framerate": "24",
"foundry.source.fullpath": "",
"foundry.source.height": "1080",
"foundry.source.layers": "colour",
"foundry.source.pixelAspect": "1",
"foundry.source.pixelAspectRatio": "",
"foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 7",
"foundry.source.reelID": "",
"foundry.source.resolution": "",
"foundry.source.samplerate": "Invalid",
"foundry.source.shortfilename": "output.%04d.exr 1000-1100",
"foundry.source.shot": "",
"foundry.source.shotDate": "",
"foundry.source.startTC": "",
"foundry.source.starttime": "1000",
"foundry.source.timecode": "87399",
"foundry.source.umid": "bbfe0c90-5b76-424a-6351-4dac36a8dde7",
"foundry.source.umidOriginator": "foundry.source.umid",
"foundry.source.width": "1920",
"foundry.timeline.autodiskcachemode": "Manual",
"foundry.timeline.colorSpace": "scene_linear",
"foundry.timeline.duration": "101",
"foundry.timeline.framerate": "24",
"foundry.timeline.outputformat": "",
"foundry.timeline.poster": "0",
"foundry.timeline.posterLayer": "colour",
"foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=",
"foundry.timeline.samplerate": "Invalid",
"isSequence": true,
"media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}",
"media.exr.compression": "2",
"media.exr.compressionName": "Zip (1 scanline)",
"media.exr.dataWindow": "1,1,1918,1078",
"media.exr.displayWindow": "0,0,1919,1079",
"media.exr.lineOrder": "0",
"media.exr.nuke.input.frame_rate": "24",
"media.exr.nuke.input.timecode": "01:00:41:15",
"media.exr.pixelAspectRatio": "1",
"media.exr.screenWindowCenter": "0,0",
"media.exr.screenWindowWidth": "1",
"media.exr.type": "scanlineimage",
"media.exr.version": "1",
"media.input.bitsperchannel": "16-bit half float",
"media.input.ctime": "2024-09-23 08:37:23",
"media.input.filereader": "exr",
"media.input.filesize": "1095868",
"media.input.frame": "1",
"media.input.frame_rate": "24",
"media.input.height": "1080",
"media.input.mtime": "2024-09-23 08:37:23",
"media.input.timecode": "01:00:41:15",
"media.input.width": "1920",
"media.nuke.full_layer_names": "0",
"media.nuke.node_hash": "9b",
"media.nuke.version": "15.1v2",
"openpype.source.colourtransform": "scene_linear",
"openpype.source.height": 1080,
"openpype.source.pixelAspect": 1.0,
"openpype.source.width": 1920,
"padding": 4
},
"name": "",
"available_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 101.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 1000.0
}
},
"available_image_bounds": null,
"target_url_base": "C:\\with_tc",
"name_prefix": "output.",
"name_suffix": ".exr",
"start_frame": 1000,
"frame_step": 1,
"rate": 24.0,
"frame_zero_padding": 4,
"missing_frame_policy": "error"
}
},
"active_media_reference_key": "DEFAULT_MEDIA"
},
{
"OTIO_SCHEMA": "Clip.2",
"metadata": {},
"name": "output",
"source_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 11.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 0.0
}
},
"effects": [],
"markers": [],
"enabled": true,
"media_references": {
"DEFAULT_MEDIA": {
"OTIO_SCHEMA": "ImageSequenceReference.1",
"metadata": {
"clip.properties.blendfunc": "0",
"clip.properties.colourspacename": "default ()",
"clip.properties.domainroot": "",
"clip.properties.enabled": "1",
"clip.properties.expanded": "1",
"clip.properties.opacity": "1",
"clip.properties.valuesource": "",
"foundry.source.audio": "",
"foundry.source.bitmapsize": "0",
"foundry.source.bitsperchannel": "0",
"foundry.source.channelformat": "integer",
"foundry.source.colourtransform": "scene_linear",
"foundry.source.duration": "101",
"foundry.source.filename": "output.%04d.exr 1000-1100",
"foundry.source.filesize": "",
"foundry.source.fragments": "101",
"foundry.source.framerate": "24",
"foundry.source.fullpath": "",
"foundry.source.height": "1080",
"foundry.source.layers": "colour",
"foundry.source.pixelAspect": "1",
"foundry.source.pixelAspectRatio": "",
"foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 7",
"foundry.source.reelID": "",
"foundry.source.resolution": "",
"foundry.source.samplerate": "Invalid",
"foundry.source.shortfilename": "output.%04d.exr 1000-1100",
"foundry.source.shot": "",
"foundry.source.shotDate": "",
"foundry.source.startTC": "",
"foundry.source.starttime": "1000",
"foundry.source.timecode": "87399",
"foundry.source.umid": "bbfe0c90-5b76-424a-6351-4dac36a8dde7",
"foundry.source.umidOriginator": "foundry.source.umid",
"foundry.source.width": "1920",
"foundry.timeline.autodiskcachemode": "Manual",
"foundry.timeline.colorSpace": "scene_linear",
"foundry.timeline.duration": "101",
"foundry.timeline.framerate": "24",
"foundry.timeline.outputformat": "",
"foundry.timeline.poster": "0",
"foundry.timeline.posterLayer": "colour",
"foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=",
"foundry.timeline.samplerate": "Invalid",
"isSequence": true,
"media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}",
"media.exr.compression": "2",
"media.exr.compressionName": "Zip (1 scanline)",
"media.exr.dataWindow": "1,1,1918,1078",
"media.exr.displayWindow": "0,0,1919,1079",
"media.exr.lineOrder": "0",
"media.exr.nuke.input.frame_rate": "24",
"media.exr.nuke.input.timecode": "01:00:41:15",
"media.exr.pixelAspectRatio": "1",
"media.exr.screenWindowCenter": "0,0",
"media.exr.screenWindowWidth": "1",
"media.exr.type": "scanlineimage",
"media.exr.version": "1",
"media.input.bitsperchannel": "16-bit half float",
"media.input.ctime": "2024-09-23 08:37:23",
"media.input.filereader": "exr",
"media.input.filesize": "1095868",
"media.input.frame": "1",
"media.input.frame_rate": "24",
"media.input.height": "1080",
"media.input.mtime": "2024-09-23 08:37:23",
"media.input.timecode": "01:00:41:15",
"media.input.width": "1920",
"media.nuke.full_layer_names": "0",
"media.nuke.node_hash": "9b",
"media.nuke.version": "15.1v2",
"openpype.source.colourtransform": "scene_linear",
"openpype.source.height": 1080,
"openpype.source.pixelAspect": 1.0,
"openpype.source.width": 1920,
"padding": 4
},
"name": "",
"available_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 101.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 1000.0
}
},
"available_image_bounds": null,
"target_url_base": "C:\\with_tc",
"name_prefix": "output.",
"name_suffix": ".exr",
"start_frame": 1000,
"frame_step": 1,
"rate": 24.0,
"frame_zero_padding": 4,
"missing_frame_policy": "error"
}
},
"active_media_reference_key": "DEFAULT_MEDIA"
}
],
"kind": "Video"
}

View file

@ -37,27 +37,31 @@ class CaptureFFmpegCalls():
return ["/path/to/ffmpeg"]
def run_process(file_name: str):
def run_process(file_name: str, instance_data: dict = None):
"""
"""
# Get OTIO review data from serialized file_name
file_path = os.path.join(_RESOURCE_DIR, file_name)
clip = otio.schema.Clip.from_json_file(file_path)
# Prepare dummy instance and capture call object
capture_call = CaptureFFmpegCalls()
processor = extract_otio_review.ExtractOTIOReview()
Anatomy = NamedTuple("Anatomy", project_name=str)
instance = MockInstance(
{
if not instance_data:
# Get OTIO review data from serialized file_name
file_path = os.path.join(_RESOURCE_DIR, file_name)
clip = otio.schema.Clip.from_json_file(file_path)
instance_data = {
"otioReviewClips": [clip],
"handleStart": 10,
"handleEnd": 10,
"workfileFrameStart": 1001,
"folderPath": "/dummy/path",
"anatomy": Anatomy("test_project"),
}
)
instance_data.update({
"folderPath": "/dummy/path",
"anatomy": Anatomy("test_project"),
})
instance = MockInstance(instance_data)
# Mock calls to extern and run plugins.
with mock.patch.object(
@ -73,9 +77,14 @@ def run_process(file_name: str):
with mock.patch.object(
processor,
"_get_folder_name_based_prefix",
return_value="C:/result/output."
return_value="output."
):
processor.process(instance)
with mock.patch.object(
processor,
"staging_dir",
return_value="C:/result/"
):
processor.process(instance)
# return all calls made to ffmpeg subprocess
return capture_call.calls
@ -103,7 +112,7 @@ def test_image_sequence_with_embedded_tc_and_handles_out_of_range():
# Report from source exr (1001-1101) with enforce framerate
"/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i "
"C:\\exr_embedded_tc\\output.%04d.exr -start_number 1001 "
f"C:\\exr_embedded_tc{os.sep}output.%04d.exr -start_number 1001 "
"C:/result/output.%03d.jpg"
]
@ -133,7 +142,7 @@ def test_image_sequence_and_handles_out_of_range():
# 1001-1095 = source range conformed to 25fps
# 1096-1096 = additional 1 tail frames
"/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i "
"C:\\tif_seq\\output.%04d.tif -start_number 996 C:/result/output.%03d.jpg"
f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996 C:/result/output.%03d.jpg"
]
assert calls == expected
@ -203,3 +212,119 @@ def test_short_movie_tail_gap_handles():
]
assert calls == expected
def test_multiple_review_clips_no_gap():
"""
Use multiple review clips (image sequence).
Timeline 25fps
"""
file_path = os.path.join(_RESOURCE_DIR, "multiple_review_clips.json")
clips = otio.schema.Track.from_json_file(file_path)
instance_data = {
"otioReviewClips": clips,
"handleStart": 10,
"handleEnd": 10,
"workfileFrameStart": 1001,
}
calls = run_process(
None,
instance_data=instance_data
)
expected = [
# 10 head black frames generated from gap (991-1000)
'/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune '
'stillimage -start_number 991 C:/result/output.%03d.jpg',
# Alternance 25fps tiff sequence and 24fps exr sequence for 100 frames each
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1001 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i '
f'C:\\with_tc{os.sep}output.%04d.exr '
'-start_number 1102 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1199 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i '
f'C:\\with_tc{os.sep}output.%04d.exr '
'-start_number 1300 C:/result/output.%03d.jpg',
# Repeated 25fps tiff sequence multiple times till the end
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1397 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1498 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1599 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1700 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1801 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1902 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 2003 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 2104 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 2205 C:/result/output.%03d.jpg'
]
assert calls == expected
def test_multiple_review_clips_with_gap():
"""
Use multiple review clips (image sequence) with gap.
Timeline 24fps
"""
file_path = os.path.join(_RESOURCE_DIR, "multiple_review_clips_gap.json")
clips = otio.schema.Track.from_json_file(file_path)
instance_data = {
"otioReviewClips": clips,
"handleStart": 10,
"handleEnd": 10,
"workfileFrameStart": 1001,
}
calls = run_process(
None,
instance_data=instance_data
)
expected = [
# Gap on review track (12 frames)
'/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi -i color=c=black:s=1280x720 -tune '
'stillimage -start_number 991 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i '
f'C:\\with_tc{os.sep}output.%04d.exr '
'-start_number 1003 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i '
f'C:\\with_tc{os.sep}output.%04d.exr '
'-start_number 1091 C:/result/output.%03d.jpg'
]
assert calls == expected