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                                 "&lt;producer&gt;",
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 = '&lt;producer&gt;'
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))