File indexing completed on 2024-05-12 16:21:32
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 """Kdenlive (MLT XML) Adapter.""" 0008 import re 0009 import os 0010 import sys 0011 from xml.etree import ElementTree as ET 0012 from xml.dom import minidom 0013 import opentimelineio as otio 0014 import json 0015 try: 0016 from urllib.parse import urlparse, unquote 0017 except ImportError: 0018 # Python 2 0019 from urlparse import urlparse 0020 from urllib import unquote 0021 0022 marker_types = { 0023 0: (otio.schema.MarkerColor.PURPLE, '#9b59b6'), 0024 1: (otio.schema.MarkerColor.CYAN, '#3daee9'), 0025 2: (otio.schema.MarkerColor.BLUE, '#1abc9c'), 0026 3: (otio.schema.MarkerColor.GREEN, '#1cdc9a'), 0027 4: (otio.schema.MarkerColor.YELLOW, '#c9ce3b'), 0028 5: (otio.schema.MarkerColor.ORANGE, '#fdbc4b'), 0029 6: (otio.schema.MarkerColor.RED, '#f39c1f'), 0030 7: (otio.schema.MarkerColor.PINK, '#f47750'), 0031 8: (otio.schema.MarkerColor.MAGENTA, '#da4453') 0032 } 0033 0034 marker_categories = {} 0035 0036 0037 def read_property(element, name): 0038 """Decode an MLT item property 0039 which value is contained in a "property" XML element 0040 with matching "name" attribute""" 0041 return element.findtext("property[@name='{}']".format(name), '') 0042 0043 0044 def time(clock, rate): 0045 """Decode an MLT time 0046 which is either a frame count or a timecode string 0047 after format hours:minutes:seconds.floatpart""" 0048 hms = [float(x) for x in clock.replace(',', '.').split(':')] 0049 if len(hms) > 1: 0050 smh = list(reversed(hms)) 0051 hours = smh[2] if len(hms) > 2 else 0 0052 mins = smh[1] 0053 secs = smh[0] 0054 # unpick the same rounding/flooring from the clock function 0055 # (N.B.: code from the clock function mimicks what 0056 # I've seen from the mlt source code. 0057 # It's technically wrong. Or at least, I believe 0058 # it's written assuming an integer frame rate) 0059 f = ( 0060 round(secs * rate) 0061 + int(mins * 60 * rate) 0062 + int(hours * 3600 * rate) 0063 ) 0064 else: 0065 f = hms[0] 0066 return otio.opentime.RationalTime(f, rate) 0067 0068 0069 def read_keyframes(kfstring, rate): 0070 """Decode MLT keyframes 0071 which are in a semicolon (;) separated list of time/value pair 0072 separated by = (linear interp) or ~= (spline) or |= (step) 0073 becomes a dict with RationalTime keys""" 0074 return dict((str(time(t, rate).value), v) 0075 for (t, v) in re.findall('([^|~=;]*)[|~]?=([^;]*)', kfstring)) 0076 0077 0078 def read_markers(markers_array, json_string, rate): 0079 """Convert Kdenlive's marker structure (JSON string) to otio markers""" 0080 if json_string: 0081 markers = json.loads(json_string) 0082 for json_marker in markers: 0083 time_range = otio.opentime.TimeRange( 0084 otio.opentime.RationalTime(json_marker["pos"], rate), 0085 otio.opentime.RationalTime(0, rate) 0086 ) 0087 marker = otio.schema.Marker( 0088 name=json_marker["comment"], 0089 marked_range=time_range, 0090 color=marker_types[json_marker["type"]][0] 0091 ) 0092 markers_array.append(marker) 0093 0094 0095 def read_mix(mix, rate): 0096 value = read_property(mix, 'kdenlive:mixcut') 0097 if value == '': 0098 # missing mixcut property: this is a transition, but not a mix 0099 return None, None, None, None 0100 before_mix_cut = time(value, rate) 0101 0102 mix_range = otio.opentime.TimeRange.range_from_start_end_time( 0103 start_time=time(mix.get('in'), rate), 0104 end_time_exclusive=(time(mix.get('out'), rate)) 0105 ) 0106 after_mix_cut = mix_range.duration - before_mix_cut 0107 reverse = bool(int(read_property(mix, 'reverse'))) 0108 0109 return mix_range, before_mix_cut, after_mix_cut, reverse 0110 0111 0112 def item_from_xml(xml_item, rate, byid, bin_producer_name): 0113 """Create an otio item from xml""" 0114 if xml_item.tag == 'blank': 0115 # the item is a gap 0116 gap = otio.schema.Gap( 0117 duration=time(xml_item.get('length'), rate)) 0118 return gap 0119 elif xml_item.tag == 'entry': 0120 # the item is a link to a producer 0121 producer = byid[xml_item.get('producer')] 0122 service = read_property(producer, 'mlt_service') 0123 available_range = otio.opentime.TimeRange.range_from_start_end_time( 0124 start_time=time(producer.get('in'), rate), 0125 end_time_exclusive=( 0126 time(producer.get('out'), rate) 0127 + otio.opentime.RationalTime(1, rate) 0128 ), 0129 ) 0130 source_range = otio.opentime.TimeRange.range_from_start_end_time( 0131 start_time=time(xml_item.get('in'), rate), 0132 end_time_exclusive=( 0133 time(xml_item.get('out'), rate) 0134 + otio.opentime.RationalTime(1, rate) 0135 ), 0136 ) 0137 # media reference clip 0138 reference = None 0139 if service in ['avformat', 'avformat-novalidate', 'qimage']: 0140 # producer is a file based clip 0141 reference = otio.schema.ExternalReference( 0142 target_url=read_property( 0143 producer, 'kdenlive:originalurl') or 0144 read_property(producer, 'resource'), 0145 available_range=available_range) 0146 elif service == 'color': 0147 # producer is a color clip 0148 reference = otio.schema.GeneratorReference( 0149 generator_kind='SolidColor', 0150 parameters={'color': read_property(producer, 'resource')}, 0151 available_range=available_range) 0152 elif (service == 'frei0r.test_pat_B' 0153 and read_property(producer, '0') == '4'): 0154 # producer is a smpt bar clip 0155 reference = otio.schema.GeneratorReference( 0156 generator_kind='SMPTEBars', 0157 available_range=available_range) 0158 clip = otio.schema.Clip( 0159 name=read_property(producer, 'kdenlive:clipname'), 0160 source_range=source_range, 0161 media_reference=reference or otio.schema.MissingReference()) 0162 # process clip markers, they are only stored in the bin producer 0163 bin_producer = byid[bin_producer_name[read_property(producer, 'kdenlive:id')]] 0164 read_markers(clip.markers, 0165 read_property(bin_producer, 'kdenlive:markers'), rate) 0166 # process effects 0167 for effect in xml_item.findall('filter'): 0168 kdenlive_id = read_property(effect, 'kdenlive_id') 0169 if kdenlive_id in ['fadein', 'fade_from_black', 0170 'fadeout', 'fade_to_black']: 0171 clip.effects.append(otio.schema.Effect( 0172 effect_name=kdenlive_id, 0173 metadata={'duration': 0174 time(effect.get('out'), rate) 0175 - time(effect.get('in', 0176 producer.get('in')), rate) 0177 })) 0178 elif kdenlive_id in ['volume', 'brightness']: 0179 clip.effects.append(otio.schema.Effect( 0180 effect_name=kdenlive_id, 0181 metadata={'keyframes': read_keyframes( 0182 read_property(effect, 'level'), rate)})) 0183 return clip 0184 return None 0185 0186 0187 def resize_item(item, delta, right): 0188 """Resize an item and keep its position (no ripple) 0189 by resizing the neighbors too""" 0190 item.source_range = otio.opentime.TimeRange( 0191 start_time=(item.source_range.start_time 0192 - (delta if not right else otio.opentime.RationalTime(0))), 0193 duration=item.source_range.duration + delta 0194 ) 0195 if right: 0196 after = item.parent().neighbors_of(item)[1] 0197 if after: 0198 after.source_range = otio.opentime.TimeRange( 0199 start_time=after.source_range.start_time, 0200 duration=after.source_range.duration - delta 0201 ) 0202 else: 0203 before = item.parent().neighbors_of(item)[0] 0204 if before: 0205 before.source_range = otio.opentime.TimeRange( 0206 start_time=before.source_range.start_time, 0207 duration=before.source_range.duration - delta 0208 ) 0209 0210 0211 def read_from_string(input_str): 0212 """Read a Kdenlive project (MLT XML) 0213 Kdenlive uses a given MLT project layout, similar to Shotcut, 0214 combining a "main_bin" playlist to organize source media, 0215 and a "global_feed" tractor for timeline. 0216 Timeline tracks include virtual sub-track, 0217 used for same-track transitions""" 0218 mlt, byid = ET.XMLID(input_str) 0219 profile = mlt.find('profile') 0220 rate = (float(profile.get('frame_rate_num')) 0221 / float(profile.get('frame_rate_den', 1))) 0222 0223 main_bin = mlt.find("playlist[@id='main_bin']") 0224 bin_producer_name = {} 0225 for entry in main_bin.findall('entry'): 0226 producer = byid[entry.get('producer')] 0227 kdenlive_id = read_property(producer, 'kdenlive:id') 0228 bin_producer_name[kdenlive_id] = producer.get('id') 0229 0230 timeline = otio.schema.Timeline( 0231 name=mlt.get('name', 'Kdenlive imported timeline')) 0232 0233 maintractor = mlt.find("tractor[@global_feed='1']") 0234 # global_feed is no longer set in newer kdenlive versions 0235 if maintractor is None: 0236 alltractors = mlt.findall("tractor") 0237 # the last tractor is the main tractor 0238 maintractor = alltractors[-1] 0239 # check all other tractors are used as tracks 0240 for tractor in alltractors[:-1]: 0241 if maintractor.find("track[@producer='%s']" % tractor.attrib['id']) is None: 0242 raise RuntimeError("Can't find main tractor") 0243 0244 for maintrack in maintractor.findall('track'): 0245 if maintrack.get('producer') == 'black_track': 0246 continue 0247 subtractor = byid[maintrack.get('producer')] 0248 stack = otio.schema.Stack() 0249 0250 subtracks = subtractor.findall('track') 0251 for sub in subtracks: 0252 mixTrack = otio.schema.Track() 0253 playlist = byid[sub.get('producer')] 0254 for xml_item in playlist.iter(): 0255 item = item_from_xml(xml_item, rate, byid, bin_producer_name) 0256 if item: 0257 mixTrack.append(item) 0258 if mixTrack.find_clips(): 0259 stack.append(mixTrack) 0260 0261 # process "mixes" (same-track-transitions) 0262 mixes = subtractor.findall('transition') 0263 0264 # 1. step: flaten internal mix tracks to one track 0265 for mix in mixes: 0266 (mix_range, before_mix_cut, 0267 after_mix_cut, reverse) = read_mix(mix, rate) 0268 if mix_range is None: 0269 continue 0270 0271 found_clip = stack[0].find_clips(search_range=mix_range)[0] 0272 resize_item(found_clip, 0273 - (after_mix_cut if reverse else before_mix_cut), 0274 not reverse) 0275 0276 found_clip = stack[1].find_clips(search_range=mix_range)[0] 0277 resize_item(found_clip, 0278 - (before_mix_cut if reverse else after_mix_cut), 0279 reverse) 0280 0281 track = otio.algorithms.flatten_stack(stack) 0282 0283 # 2. step: build and insert transitions 0284 for mix in mixes: 0285 (mix_range, before_mix_cut, 0286 after_mix_cut, reverse) = read_mix(mix, rate) 0287 if mix_range is None: 0288 continue 0289 0290 found_clip = track.find_clips( 0291 search_range=otio.opentime.TimeRange.range_from_start_end_time( 0292 start_time=(time(mix.get('in'), rate) 0293 - otio.opentime.RationalTime( 0294 1 if before_mix_cut.value == 0 else 1)), 0295 end_time_exclusive=(time(mix.get('out'), rate)) 0296 ))[0] 0297 index = track.index(found_clip) 0298 track.insert(index + 1, otio.schema.Transition( 0299 transition_type=otio.schema.TransitionTypes.SMPTE_Dissolve, 0300 in_offset=after_mix_cut, 0301 out_offset=before_mix_cut 0302 )) 0303 0304 track.name = read_property(subtractor, 'kdenlive:track_name') 0305 if bool(read_property(subtractor, 'kdenlive:audio_track')): 0306 track.kind = otio.schema.TrackKind.Audio 0307 else: 0308 track.kind = otio.schema.TrackKind.Video 0309 timeline.tracks.append(track) 0310 0311 # process "compositions" (transitions between clips in different tracks) 0312 for transition in maintractor.findall('transition'): 0313 kdenlive_id = read_property(transition, 'kdenlive_id') 0314 if kdenlive_id == 'wipe': 0315 b_track = int(read_property(transition, 'b_track')) 0316 timeline.tracks[b_track - 1].append( 0317 otio.schema.Transition( 0318 transition_type=otio.schema.TransitionTypes.SMPTE_Dissolve, 0319 in_offset=time(transition.get('in'), rate), 0320 out_offset=time(transition.get('out'), rate))) 0321 0322 # process timeline markers 0323 read_markers(timeline.tracks.markers, 0324 read_property(main_bin, "kdenlive:docproperties.guides"), 0325 rate) 0326 0327 return timeline 0328 0329 0330 def write_property(element, name, value): 0331 """Store an MLT property 0332 value contained in a "property" sub element 0333 with defined "name" attribute""" 0334 property = ET.SubElement(element, 'property', {'name': name}) 0335 property.text = value 0336 0337 0338 def clock(time): 0339 """Encode time to an MLT timecode string 0340 after format hours:minutes:seconds.floatpart""" 0341 frames = time.to_frames() 0342 hours = int(frames / (time.rate * 3600)) 0343 frames -= int(hours * 3600 * time.rate) 0344 mins = int(frames / (time.rate * 60)) 0345 frames -= int(mins * 60 * time.rate) 0346 secs = frames / time.rate 0347 return "%02d:%02d:%06.3f" % (hours, mins, secs) 0348 0349 0350 def write_keyframes(kfdict): 0351 """Build a MLT keyframe string""" 0352 return ';'.join('{}={}'.format(t, v) 0353 for t, v in kfdict.items()) 0354 0355 0356 def write_markers(markers): 0357 """Convert otio markers to Kdenlive's marker structure (JSON string)""" 0358 markers_array = [] 0359 for marker in markers: 0360 try: 0361 marker_type = [ 0362 key for key in marker_types.items() if key[1][0] == marker.color 0363 ][0][0] 0364 except Exception: 0365 marker_type = 0 0366 markers_array.append( 0367 { 0368 "pos": marker.marked_range.start_time.to_frames(), 0369 "comment": marker.name, 0370 "type": marker_type 0371 } 0372 ) 0373 if marker_type not in marker_categories: 0374 # Since Kdenlive 22.12.0 there are no static build-in 0375 # categories anymore instead you can create as many 0376 # custom categories as you want. 0377 # Hence we need to create categories now. 0378 marker_categories[marker_type] = { 0379 "color": marker_types[marker_type][1], 0380 "comment": f"Category {len(marker_categories)+1}", 0381 "index": marker_type 0382 } 0383 return json.dumps(markers_array) 0384 0385 0386 def write_to_string(input_otio): 0387 """Write a timeline to Kdenlive project 0388 Re-creating the bin storing all used source clips 0389 and constructing the tracks""" 0390 if (not isinstance(input_otio, otio.schema.Timeline) 0391 and len(input_otio) > 1): 0392 print('WARNING: Only one timeline supported, using the first one.') 0393 input_otio = input_otio[0] 0394 # Project header & metadata 0395 mlt = ET.Element( 0396 'mlt', 0397 dict( 0398 version="6.23.0", 0399 title=input_otio.name, 0400 LC_NUMERIC="C", 0401 producer="main_bin", 0402 ), 0403 ) 0404 rate = input_otio.duration().rate 0405 (rate_num, rate_den) = { 0406 23.98: (24000, 1001), 0407 29.97: (30000, 1001), 0408 59.94: (60000, 1001) 0409 }.get(round(float(rate), 2), (int(rate), 1)) 0410 ET.SubElement( 0411 mlt, 'profile', 0412 dict( 0413 description='HD 1080p {} fps'.format(rate), 0414 frame_rate_num=str(rate_num), 0415 frame_rate_den=str(rate_den), 0416 width='1920', 0417 height='1080', 0418 display_aspect_num='16', 0419 display_aspect_den='9', 0420 sample_aspect_num='1', 0421 sample_aspect_den='1', 0422 colorspace='709', 0423 progressive='1', 0424 ), 0425 ) 0426 0427 # Build media library, indexed by url 0428 main_bin = ET.Element('playlist', dict(id='main_bin')) 0429 write_property(main_bin, 'kdenlive:docproperties.decimalPoint', '.') 0430 write_property(main_bin, 'kdenlive:docproperties.version', '0.98') 0431 write_property(main_bin, 'xml_retain', '1') 0432 0433 # Process timeline markers 0434 write_property(main_bin, 'kdenlive:docproperties.guides', 0435 write_markers(input_otio.tracks.markers)) 0436 0437 producer_count = 0 0438 0439 media_prod = {} 0440 producer_array = {} 0441 for clip in input_otio.find_clips(): 0442 producer, producer_count, key = _make_producer( 0443 producer_count, clip, mlt, rate, media_prod 0444 ) 0445 if key is None: 0446 continue 0447 if producer is None: 0448 # There is already a producer for this clip 0449 # make sure it covers the clip's range 0450 producer = producer_array[key] 0451 prod_range = otio.opentime.TimeRange.range_from_start_end_time_inclusive( 0452 time(producer.get('in'), rate), 0453 time(producer.get('out'), rate) 0454 ).extended_by(clip.source_range) 0455 producer.set('in', clock(prod_range.start_time)) 0456 producer.set('out', clock(prod_range.end_time_inclusive())) 0457 write_property( 0458 producer, 'length', 0459 str(prod_range.duration.to_frames()) 0460 ) 0461 producer_array[key] = producer 0462 0463 for key in producer_array: 0464 producer = producer_array[key] 0465 producer_id = producer.get('id') 0466 kdenlive_id = read_property(producer, 'kdenlive:id') 0467 entry_in = producer.get('in') 0468 entry_out = producer.get('out') 0469 entry = ET.SubElement( 0470 main_bin, 'entry', 0471 { 0472 'producer': producer_id, 0473 'in': entry_in, 0474 'out': entry_out, 0475 }, 0476 ) 0477 write_property(entry, 'kdenlive:id', kdenlive_id) 0478 0479 mlt.append(main_bin) 0480 0481 # Background clip 0482 black = ET.SubElement(mlt, 'producer', {'id': 'black_track'}) 0483 write_property(black, 'resource', 'black') 0484 write_property(black, 'mlt_service', 'color') 0485 0486 # Timeline & tracks 0487 maintractor = ET.Element('tractor', {'global_feed': '1'}) 0488 ET.SubElement(maintractor, 'track', {'producer': 'black_track'}) 0489 0490 unsupported_count = 0 0491 0492 for i, track in enumerate(input_otio.tracks): 0493 is_audio = track.kind == otio.schema.TrackKind.Audio 0494 0495 tractor_id = 'tractor{}'.format(i) 0496 subtractor = ET.Element('tractor', dict(id=tractor_id)) 0497 write_property(subtractor, 'kdenlive:track_name', track.name) 0498 0499 ET.SubElement( 0500 maintractor, 'track', dict(producer=tractor_id) 0501 ) 0502 0503 playlist = _make_playlist( 0504 2 * i, 0505 "video" if is_audio else "audio", 0506 subtractor, 0507 mlt, 0508 ) 0509 dummy_playlist = _make_playlist(2 * i + 1, "both", subtractor, mlt) 0510 0511 if is_audio: 0512 write_property(subtractor, 'kdenlive:audio_track', '1') 0513 write_property(playlist, 'kdenlive:audio_track', '1') 0514 0515 # Track playlist 0516 for item in track: 0517 if isinstance(item, otio.schema.Gap): 0518 ET.SubElement( 0519 playlist, 'blank', dict(length=clock(item.duration())) 0520 ) 0521 elif isinstance(item, otio.schema.Clip): 0522 producer_id = "unsupported" 0523 reset_range = otio.opentime.TimeRange( 0524 start_time=otio.opentime.RationalTime(0), 0525 duration=item.source_range.duration, 0526 ) 0527 clip_in = reset_range.start_time 0528 clip_out = reset_range.end_time_inclusive() 0529 kdenlive_id = "3" 0530 if isinstance(item.media_reference, 0531 otio.schema.ExternalReference): 0532 key = _prod_key_from_item(item, is_audio) 0533 producer_id, kdenlive_id = media_prod[ 0534 key 0535 ] 0536 speed = key[2] 0537 if speed is None: 0538 speed = 1 0539 source_range = otio.opentime.TimeRange( 0540 otio.opentime.RationalTime( 0541 item.source_range.start_time.value / speed, 0542 item.source_range.start_time.rate, 0543 ), 0544 item.source_range.duration, 0545 ) 0546 clip_in = source_range.start_time 0547 clip_out = source_range.end_time_inclusive() 0548 elif isinstance(item.media_reference, otio.schema.GeneratorReference): 0549 if item.media_reference.generator_kind == 'SolidColor': 0550 producer_id, kdenlive_id = media_prod[ 0551 ( 0552 "color", 0553 item.media_reference.parameters['color'], 0554 None, 0555 None, 0556 ) 0557 ] 0558 elif item.media_reference.generator_kind == 'SMPTEBars': 0559 producer_id, kdenlive_id = media_prod[ 0560 ( 0561 "frei0r.test_pat_B", 0562 "<producer>", 0563 None, 0564 None, 0565 ) 0566 ] 0567 0568 if producer_id == "unsupported": 0569 unsupported_count += 1 0570 0571 entry = ET.SubElement( 0572 playlist, 'entry', 0573 { 0574 'producer': producer_id, 0575 'in': clock(clip_in), 0576 'out': clock(clip_out), 0577 }, 0578 ) 0579 write_property(entry, 'kdenlive:id', kdenlive_id) 0580 0581 # Clip effects 0582 for effect in item.effects: 0583 kid = effect.effect_name 0584 if kid in ['fadein', 'fade_from_black']: 0585 filt = ET.SubElement( 0586 entry, 'filter', 0587 { 0588 "in": clock(clip_in), 0589 "out": clock(clip_in + effect.metadata['duration']), 0590 }, 0591 ) 0592 write_property(filt, 'kdenlive_id', kid) 0593 write_property(filt, 'end', '1') 0594 if kid == 'fadein': 0595 write_property(filt, 'mlt_service', 'volume') 0596 write_property(filt, 'gain', '0') 0597 else: 0598 write_property(filt, 'mlt_service', 'brightness') 0599 write_property(filt, 'start', '0') 0600 elif effect.effect_name in ['fadeout', 'fade_to_black']: 0601 filt = ET.SubElement( 0602 entry, 'filter', 0603 { 0604 "in": clock(clip_out - effect.metadata['duration']), 0605 "out": clock(clip_out), 0606 }, 0607 ) 0608 write_property(filt, 'kdenlive_id', kid) 0609 write_property(filt, 'end', '0') 0610 if kid == 'fadeout': 0611 write_property(filt, 'mlt_service', 'volume') 0612 write_property(filt, 'gain', '1') 0613 else: 0614 write_property(filt, 'mlt_service', 'brightness') 0615 write_property(filt, 'start', '1') 0616 elif effect.effect_name in ['volume', 'brightness']: 0617 filt = ET.SubElement(entry, 'filter') 0618 write_property(filt, 'kdenlive_id', kid) 0619 write_property(filt, 'mlt_service', kid) 0620 write_property(filt, 'level', 0621 write_keyframes(effect.metadata['keyframes'])) 0622 0623 elif isinstance(item, otio.schema.Transition): 0624 print('Transitions handling to be added') 0625 0626 mlt.extend((playlist, dummy_playlist, subtractor)) 0627 0628 mlt.append(maintractor) 0629 0630 # in case we need it: add substitute source clip to be referred to 0631 # when meeting an unsupported clip 0632 if unsupported_count > 0: 0633 unsupported = ET.Element( 0634 'producer', 0635 { 0636 'id': 'unsupported', 0637 'in': '0', 0638 'out': '10000', 0639 }, 0640 ) 0641 write_property(unsupported, 'mlt_service', 'qtext') 0642 write_property(unsupported, 'family', 'Courier') 0643 write_property(unsupported, 'fgcolour', '#ff808080') 0644 write_property(unsupported, 'bgcolour', '#00000000') 0645 write_property(unsupported, 'text', 0646 'Unsupported clip type') 0647 write_property(unsupported, 'kdenlive:clipname', 0648 'Placeholder: Unsupported clip type') 0649 write_property(unsupported, 'kdenlive:id', '3') 0650 mlt.insert(1, unsupported) 0651 0652 entry = ET.SubElement( 0653 main_bin, 'entry', 0654 dict(producer='unsupported'), 0655 ) 0656 write_property(entry, 'kdenlive:id', '3') 0657 0658 # Process marker/guide categories 0659 write_property(main_bin, 'kdenlive:docproperties.guidesCategories', 0660 json.dumps(list(marker_categories.values()))) 0661 0662 return minidom.parseString(ET.tostring(mlt)).toprettyxml( 0663 encoding=sys.getdefaultencoding(), 0664 ).decode(sys.getdefaultencoding()) 0665 0666 0667 def _make_playlist(count, hide, subtractor, mlt): 0668 playlist_id = 'playlist{}'.format(count) 0669 playlist = ET.Element( 0670 'playlist', 0671 dict(id=playlist_id), 0672 ) 0673 ET.SubElement( 0674 subtractor, 'track', 0675 dict( 0676 producer=playlist_id, 0677 hide=hide, 0678 ), 0679 ) 0680 return playlist 0681 0682 0683 def _decode_media_reference_url(url): 0684 return unquote(urlparse(url).path) 0685 0686 0687 def _make_producer(count, item, mlt, frame_rate, media_prod, speed=None, 0688 is_audio=None): 0689 producer = None 0690 service, resource, effect_speed, _ = _prod_key_from_item(item, is_audio) 0691 key = None 0692 if service and resource: 0693 producer_id = "producer{}".format(count) 0694 kdenlive_id = str(count + 4) # unsupported starts with id 3 0695 0696 key = (service, resource, speed, is_audio) 0697 # check not already in our library 0698 if key not in media_prod: 0699 if item.media_reference.available_range is not None: 0700 available_range = item.media_reference.available_range 0701 elif item.source_range is not None: 0702 available_range = item.source_range 0703 else: 0704 return None, count 0705 if service == "qimage": 0706 print("Image") 0707 available_range = otio.opentime.TimeRange.range_from_start_end_time( 0708 otio.opentime.RationalTime(0, frame_rate), 0709 available_range.end_time_exclusive() 0710 ) 0711 # add ids to library 0712 media_prod[key] = producer_id, kdenlive_id 0713 producer = ET.SubElement( 0714 mlt, 'producer', 0715 { 0716 'id': producer_id, 0717 'in': 0718 clock(available_range.start_time), 0719 'out': 0720 clock(available_range.end_time_inclusive()) 0721 }, 0722 ) 0723 write_property(producer, 'global_feed', '1') 0724 duration = available_range.duration.rescaled_to( 0725 frame_rate 0726 ) 0727 if speed is not None: 0728 kdenlive_id = media_prod[(service, resource, None, None)][1] 0729 write_property(producer, 'mlt_service', "timewarp") 0730 write_property(producer, 0731 'resource', ":".join((str(speed), resource))) 0732 write_property(producer, 'warp_speed', str(speed)) 0733 write_property(producer, 'warp_resource', resource) 0734 write_property(producer, 'warp_pitch', "0") 0735 write_property(producer, 0736 'set.test_audio', "0" if is_audio else "1") 0737 write_property(producer, 0738 'set.test_image', "1" if is_audio else "0") 0739 start_time = otio.opentime.RationalTime( 0740 round( 0741 available_range.start_time.value 0742 / speed 0743 ), 0744 available_range.start_time.rate, 0745 ) 0746 duration = otio.opentime.RationalTime( 0747 round(duration.value / speed), 0748 duration.rate, 0749 ) 0750 producer.set( 0751 "out", 0752 clock( 0753 otio.opentime.TimeRange( 0754 start_time, 0755 duration, 0756 ).end_time_inclusive() 0757 ), 0758 ) 0759 else: 0760 write_property(producer, 'mlt_service', service) 0761 write_property(producer, 'resource', resource) 0762 if item.name: 0763 write_property(producer, 'kdenlive:clipname', item.name) 0764 write_property( 0765 producer, 'length', 0766 str(duration.to_frames()), 0767 ) 0768 write_property(producer, 'kdenlive:id', kdenlive_id) 0769 if (isinstance(item.media_reference, 0770 otio.schema.GeneratorReference) 0771 and item.media_reference.generator_kind == 'SMPTEBars'): 0772 # set the type of the test pattern to SMPTE (value 4) 0773 write_property(producer, '0', '4') 0774 0775 count += 1 0776 0777 # create time warped version 0778 if speed is None and effect_speed is not None: 0779 # Make video resped producer 0780 _, count, _ = _make_producer( 0781 count, item, mlt, frame_rate, media_prod, effect_speed, False 0782 ) 0783 # Make audio resped producer 0784 _, count, _ = _make_producer( 0785 count, item, mlt, frame_rate, media_prod, effect_speed, True 0786 ) 0787 0788 return producer, count, key 0789 0790 0791 def _prod_key_from_item(item, is_audio): 0792 service = None 0793 resource = None 0794 speed = None 0795 if isinstance( 0796 item.media_reference, 0797 (otio.schema.ExternalReference, otio.schema.MissingReference), 0798 ): 0799 if isinstance(item.media_reference, otio.schema.ExternalReference): 0800 resource = _decode_media_reference_url(item.media_reference.target_url) 0801 elif isinstance(item.media_reference, otio.schema.MissingReference): 0802 resource = item.name 0803 0804 ext_lower = os.path.splitext(resource)[1].lower() 0805 if ext_lower == ".kdenlive": 0806 service = "xml" 0807 elif ext_lower in ( 0808 ".png", ".jpg", ".jpeg" 0809 ): 0810 service = "qimage" 0811 else: 0812 service = "avformat-novalidate" 0813 0814 for effect in item.effects: 0815 if (isinstance(effect, otio.schema.LinearTimeWarp) 0816 and not isinstance(effect, otio.schema.FreezeFrame)): 0817 if speed is None: 0818 speed = 1 0819 speed *= effect.time_scalar 0820 elif isinstance(item.media_reference, otio.schema.GeneratorReference): 0821 if item.media_reference.generator_kind == 'SolidColor': 0822 service = 'color' 0823 resource = item.media_reference.parameters['color'] 0824 elif item.media_reference.generator_kind == 'SMPTEBars': 0825 service = 'frei0r.test_pat_B' 0826 resource = '<producer>' 0827 return service, resource, speed, None if speed is None else is_audio 0828 0829 0830 if __name__ == '__main__': 0831 timeline = read_from_string( 0832 open('tests/sample_data/kdenlive_example.kdenlive', 'r').read()) 0833 print(str(timeline).replace('otio.schema', "\notio.schema")) 0834 xml = write_to_string(timeline) 0835 print(str(xml))