File indexing completed on 2024-05-05 16:55:00

0001 # -*- coding: utf-8 -*-
0002 # SPDX-License-Identifier: MIT
0003 # SPDX-FileCopyrightText: 2019-2022 Vincent Pinon <vpinon@kde.org>
0004 # SPDX-FileCopyrightText: 2021 "splidje"
0005 # SPDX-FileCopyrightText: 2022 Julius Künzel <jk.kdedev@smartlab.uber.space>
0006 
0007 import unittest
0008 import opentimelineio as otio
0009 import opentimelineio.test_utils as otio_test_utils
0010 import otio_kdenlive_adapter.adapters.kdenlive as kdenlive_adapter
0011 import os
0012 from xml.etree import ElementTree as ET
0013 
0014 
0015 def prepare_for_check(timeline):
0016     """Clear the given timeline of irrelevant data
0017     For example since Kdenlive only supports one timeline,
0018     we do not care about its name. Same applies to reference names."""
0019     timeline.name = ""
0020     for track in timeline.tracks:
0021         for clip in track.find_clips():
0022             if isinstance(clip.media_reference, list):
0023                 for reference in clip.media_reference:
0024                     reference.name = ""
0025             else:
0026                 clip.media_reference.name = ""
0027 
0028 
0029 class AdaptersKdenliveTest(unittest.TestCase, otio_test_utils.OTIOAssertions):
0030 
0031     def __init__(self, *args, **kwargs):
0032         super(AdaptersKdenliveTest, self).__init__(*args, **kwargs)
0033 
0034     def test_library_roundtrip(self):
0035         timeline = otio.adapters.read_from_file(
0036             os.path.join(os.path.dirname(__file__), "sample_data",
0037                          "kdenlive_example_v221170.kdenlive"))
0038 
0039         # check tracks
0040         self.assertIsNotNone(timeline)
0041         self.assertEqual(len(timeline.tracks), 5)
0042 
0043         self.assertEqual(len(timeline.video_tracks()), 2)
0044         self.assertEqual(len(timeline.audio_tracks()), 3)
0045 
0046         # check clips
0047         clip_urls = (('AUD0002.OGG',),
0048                      ('AUD0001.OGG', 'AUD0001.OGG'),
0049                      ('VID0001.MKV', 'VID0001.MKV'),
0050                      ('VID0001.MKV', 'VID0001.MKV'),
0051                      ('VID0002.MKV', 'VID0003.MKV'))
0052 
0053         for n, track in enumerate(timeline.tracks):
0054             self.assertTupleEqual(
0055                 clip_urls[n],
0056                 tuple(c.media_reference.target_url
0057                       for c in track
0058                       if isinstance(c, otio.schema.Clip) and
0059                       isinstance(
0060                           c.media_reference,
0061                           otio.schema.ExternalReference)))
0062 
0063         # check timeline markers
0064         self.assertEqual(len(timeline.tracks.markers), 2)
0065 
0066         markers_data = ((230, 'Purple Marker', otio.schema.MarkerColor.PURPLE),
0067                         (466, 'Green', otio.schema.MarkerColor.GREEN))
0068 
0069         for n, marker in enumerate(timeline.tracks.markers):
0070             self.assertEqual(0, marker.marked_range.duration.to_frames())
0071             self.assertEqual(markers_data[n][0],
0072                              marker.marked_range.start_time.to_frames())
0073             self.assertEqual(markers_data[n][1], marker.name)
0074             self.assertEqual(markers_data[n][2], marker.color)
0075 
0076         kdenlive_xml = otio.adapters.write_to_string(timeline, "kdenlive")
0077         self.assertIsNotNone(kdenlive_xml)
0078 
0079         new_timeline = otio.adapters.read_from_string(kdenlive_xml, "kdenlive")
0080         self.assertJsonEqual(timeline, new_timeline)
0081 
0082     def test_v19_11_80__file_roundtrip(self):
0083         timeline = otio.adapters.read_from_file(
0084             os.path.join(os.path.dirname(__file__), "sample_data",
0085                          "kdenlive_example_v191180.kdenlive"))
0086 
0087         self.assertIsNotNone(timeline)
0088         self.assertEqual(len(timeline.tracks), 5)
0089 
0090         self.assertEqual(len(timeline.video_tracks()), 2)
0091         self.assertEqual(len(timeline.audio_tracks()), 3)
0092 
0093         clip_urls = (('AUD0002.OGG',),
0094                      ('AUD0001.OGG', 'AUD0001.OGG'),
0095                      ('VID0001.MKV', 'VID0001.MKV'),
0096                      ('VID0001.MKV', 'VID0001.MKV'),
0097                      ('VID0002.MKV', 'VID0003.MKV'))
0098 
0099         for n, track in enumerate(timeline.tracks):
0100             self.assertTupleEqual(
0101                 clip_urls[n],
0102                 tuple(c.media_reference.target_url
0103                       for c in track
0104                       if isinstance(c, otio.schema.Clip) and
0105                       isinstance(
0106                           c.media_reference,
0107                           otio.schema.ExternalReference)))
0108 
0109         kdenlive_xml = otio.adapters.write_to_string(timeline, "kdenlive")
0110         self.assertIsNotNone(kdenlive_xml)
0111 
0112         new_timeline = otio.adapters.read_from_string(kdenlive_xml, "kdenlive")
0113         self.assertJsonEqual(timeline, new_timeline)
0114 
0115     def test_from_fcp_example(self):
0116         timeline = otio.adapters.read_from_file(
0117             os.path.join(
0118                 os.path.dirname(__file__),
0119                 "sample_data",
0120                 "kdenlive_example_from_fcp.xml",
0121             ),
0122         )
0123 
0124         kdenlive_xml = otio.adapters.write_to_string(timeline, "kdenlive")
0125         self.assertIsNotNone(kdenlive_xml)
0126 
0127         new_timeline = otio.adapters.read_from_string(kdenlive_xml, "kdenlive")
0128         troublesome_clip = new_timeline.video_tracks()[0][35]
0129         self.assertEqual(
0130             troublesome_clip.source_range.duration.to_frames(),
0131             807,
0132         )
0133 
0134     def test_read_mixes(self):
0135         # mixes are yet only supported to be read, not written
0136         timeline = otio.adapters.read_from_file(
0137             os.path.join(os.path.dirname(__file__), "sample_data",
0138                          "kdenlive_mixes_markers.kdenlive"))
0139 
0140         # check tracks
0141         self.assertIsNotNone(timeline)
0142         self.assertEqual(len(timeline.tracks), 4)
0143 
0144         video_tracks = timeline.video_tracks()
0145         audio_tracks = timeline.audio_tracks()
0146         self.assertEqual(len(video_tracks), 2)
0147         self.assertEqual(len(audio_tracks), 2)
0148 
0149         # check items
0150         video_track_normal = video_tracks[0]
0151         video_track_mix = video_tracks[1]
0152 
0153         audio_track_normal = audio_tracks[1]
0154         audio_track_mix = audio_tracks[0]
0155 
0156         self.assertEqual(len(video_track_normal), 10)
0157         self.assertEqual(len(audio_track_normal), 10)
0158         self.assertEqual(len(video_track_mix), 15)
0159         self.assertEqual(len(audio_track_mix), 15)
0160 
0161         clips_normal = list(video_track_normal.find_clips())
0162         self.assertEqual(len(clips_normal), 8)
0163         clips_mix = list(video_track_mix.find_clips())
0164         self.assertEqual(len(clips_mix), 8)
0165 
0166         normal_item_order = [
0167             otio.schema.Clip,
0168             otio.schema.Clip,
0169             otio.schema.Gap,
0170             otio.schema.Clip,
0171             otio.schema.Clip,
0172             otio.schema.Clip,
0173             otio.schema.Clip,
0174             otio.schema.Gap,
0175             otio.schema.Clip,
0176             otio.schema.Clip
0177         ]
0178         self.assertEqual(
0179             [type(item) for item in video_track_normal],
0180             normal_item_order
0181         )
0182         self.assertEqual(
0183             [type(item) for item in audio_track_normal],
0184             normal_item_order
0185         )
0186 
0187         mix_item_order = [
0188             otio.schema.Clip,
0189             otio.schema.Transition,
0190             otio.schema.Clip,
0191             otio.schema.Gap,
0192             otio.schema.Clip,
0193             otio.schema.Transition,
0194             otio.schema.Clip,
0195             otio.schema.Transition,
0196             otio.schema.Clip,
0197             otio.schema.Transition,
0198             otio.schema.Clip,
0199             otio.schema.Gap,
0200             otio.schema.Clip,
0201             otio.schema.Transition,
0202             otio.schema.Clip
0203         ]
0204         self.assertEqual(
0205             [type(item) for item in video_track_mix],
0206             mix_item_order
0207         )
0208         self.assertEqual(
0209             [type(item) for item in audio_track_mix],
0210             mix_item_order
0211         )
0212 
0213         def only_transitions(item):
0214             return isinstance(item, otio.schema.Transition)
0215 
0216         mix_times = ((13, 25),
0217                      (25, 25),
0218                      (0, 25),
0219                      (115, 214),
0220                      (44, 114))
0221         for x, mix in enumerate(filter(only_transitions, video_track_mix)):
0222             duration = mix.in_offset.to_frames() + mix.out_offset.to_frames()
0223             self.assertEqual(mix.in_offset.to_frames(), mix_times[x][0])
0224             self.assertEqual(duration, mix_times[x][1])
0225 
0226         mix_times = ((13, 25),
0227                      (13, 25),
0228                      (119, 131),
0229                      (13, 25),
0230                      (44, 114))
0231         for x, mix in enumerate(filter(only_transitions, audio_track_mix)):
0232             duration = mix.in_offset.to_frames() + mix.out_offset.to_frames()
0233             self.assertEqual(mix.in_offset.to_frames(), mix_times[x][0])
0234             self.assertEqual(duration, mix_times[x][1])
0235 
0236     def test_fun_read_mix(self):
0237         rate = 25
0238         mix = ET.Element(
0239             'transition',
0240             {"in": '00:00:11.000', "out": '00:00:13.000'},
0241         )
0242         mixcut = ET.SubElement(mix, 'property', {'name': 'kdenlive:mixcut'})
0243         mixcut.text = '16'
0244         reverese_prop = ET.SubElement(mix, 'property', {'name': 'reverse'})
0245         reverese_prop.text = '1'
0246         (mix_range, before_mix_cut,
0247          after_mix_cut, reverse) = kdenlive_adapter.read_mix(mix, rate)
0248         self.assertIsNotNone(mix_range)
0249         self.assertIsNotNone(before_mix_cut)
0250         self.assertIsNotNone(after_mix_cut)
0251         self.assertIsNotNone(reverse)
0252         self.assertEqual(mix_range, otio.opentime.TimeRange(
0253             start_time=otio.opentime.RationalTime(11 * rate, rate),
0254             duration=otio.opentime.RationalTime(2 * rate, rate)
0255         ))
0256         self.assertEqual(before_mix_cut, otio.opentime.RationalTime(16, rate))
0257         self.assertEqual(after_mix_cut,
0258                          otio.opentime.RationalTime(2 * rate, rate)
0259                          - otio.opentime.RationalTime(16, rate))
0260         self.assertTrue(reverse)
0261 
0262     def test_read_clip_markers(self):
0263         # clip markers are yet only supported to be read, not written
0264         timeline = otio.adapters.read_from_file(
0265             os.path.join(os.path.dirname(__file__), "sample_data",
0266                          "kdenlive_mixes_markers.kdenlive"))
0267 
0268         # check tracks
0269         self.assertIsNotNone(timeline)
0270         self.assertEqual(len(timeline.tracks), 4)
0271 
0272         video_tracks = timeline.video_tracks()
0273         audio_tracks = timeline.audio_tracks()
0274         self.assertEqual(len(video_tracks), 2)
0275         self.assertEqual(len(audio_tracks), 2)
0276 
0277         def only_clips(item):
0278             return isinstance(item, otio.schema.Clip)
0279 
0280         for track in timeline.tracks:
0281             for clip in filter(only_clips, track):
0282                 self.assertEqual(len(clip.markers), 2)
0283 
0284                 markers_data = (
0285                     (1782, 'Lila', otio.schema.MarkerColor.PURPLE),
0286                     (2899, 'Orange', otio.schema.MarkerColor.ORANGE))
0287 
0288                 for n, marker in enumerate(clip.markers):
0289                     self.assertEqual(0, marker.marked_range.duration.to_frames())
0290                     self.assertEqual(markers_data[n][0],
0291                                      marker.marked_range.start_time.to_frames())
0292                     self.assertEqual(markers_data[n][1], marker.name)
0293                     self.assertEqual(markers_data[n][2], marker.color)
0294 
0295     def test_smpte_bars(self):
0296         timeline = otio.adapters.read_from_file(
0297             os.path.join(
0298                 os.path.dirname(__file__),
0299                 "sample_data",
0300                 "generator_reference_test.otio",
0301             ),
0302         )
0303 
0304         kdenlive_xml = otio.adapters.write_to_string(timeline, "kdenlive")
0305         self.assertIsNotNone(kdenlive_xml)
0306 
0307         new_timeline = otio.adapters.read_from_string(kdenlive_xml, "kdenlive")
0308         prepare_for_check(timeline)
0309         prepare_for_check(new_timeline)
0310         self.assertIsOTIOEquivalentTo(timeline, new_timeline)
0311 
0312     def test_multiple_instances(self):
0313         # If we have multiple instances of a clip, we need to ensure that for all
0314         # of them the available_range covers the source_range.
0315         # There was a bug where the available_range and hence the bin clip only
0316         # covered the source_range of the first clip that was processed,
0317         # if available_range was None in the input file.
0318         timeline = otio.adapters.read_from_file(
0319             os.path.join(
0320                 os.path.dirname(__file__),
0321                 "sample_data",
0322                 "multiinstance.otio",
0323             ),
0324         )
0325 
0326         kdenlive_xml = otio.adapters.write_to_string(timeline, "kdenlive")
0327         self.assertIsNotNone(kdenlive_xml)
0328 
0329         new_timeline = otio.adapters.read_from_string(kdenlive_xml, "kdenlive")
0330         prepare_for_check(timeline)
0331         prepare_for_check(new_timeline)
0332 
0333         for track in new_timeline.tracks:
0334             for clip in track.find_clips():
0335                 self.assertIsNotNone(clip.media_reference.available_range)
0336                 self.assertTrue(clip.source_range.start_time
0337                                 >= clip.available_range().start_time)
0338                 self.assertTrue(clip.source_range.end_time_inclusive()
0339                                 <= clip.available_range().end_time_inclusive())
0340 
0341     def test_clock_time(self):
0342         tc = "00:00:01.040"
0343         rate = 25
0344         t = kdenlive_adapter.time(tc, rate)
0345         c = kdenlive_adapter.clock(t)
0346         self.assertEqual(tc, c)
0347 
0348 
0349 if __name__ == '__main__':
0350     print(kdenlive_adapter)
0351     unittest.main()