File indexing completed on 2025-04-20 12:54:19
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()