1#!/usr/local/bin/python3.8
2
3__author__    = 'Gilles Boccon-Gibod (bok@bok.net)'
4__copyright__ = 'Copyright 2011-2020 Axiomatic Systems, LLC.'
5
6###
7# NOTE: this script needs Bento4 command line binaries to run
8# You must place the 'mp4info' 'mp4dump', 'mp4encrypt', 'mp4fragment', and 'mp4split' binaries
9# in a directory named 'bin/<platform>' at the same level as where
10# this script is.
11# <platform> depends on the platform you're running on:
12# Mac OSX   --> platform = macosx
13# Linux x86 --> platform = linux-x86
14# Linux x64 --> platform = linux-x86_64
15# Windows   --> platform = win32
16
17from optparse import OptionParser
18import shutil
19import xml.etree.ElementTree as xml
20from xml.dom.minidom import parseString
21import tempfile
22import re
23import platform
24import sys
25import os
26import os.path as path
27import json
28import math
29import operator
30import struct
31from functools import reduce
32from subtitles import SubtitlesFile
33from mp4utils import (
34    MakePsshBox,
35    MakePsshBoxV1,
36    Base64Encode,
37    Base64Decode,
38    ComputeWidevineHeader,
39    ComputePlayReadyHeader,
40    ComputePrimetimeMetaData,
41    ComputeDolbyAc4AudioChannelConfig,
42    ComputeDolbyDigitalPlusAudioChannelConfig,
43    ComputeDolbyDigitalPlusSmoothStreamingInfo,
44    ComputeMarlinPssh,
45    Mp4IframeIndex,
46    Mp4File,
47    Mp4Encrypt,
48    Mp4Fragment,
49    Mp4Split,
50    MediaSource,
51    WalkAtoms,
52    GetEncryptionKey,
53    DerivePlayReadyKey,
54    LanguageNames,
55    LanguageCodeMap,
56    XmlDuration,
57    PrintErrorAndExit,
58    MakeNewDir,
59    BooleanFromString
60)
61
62# setup main options
63VERSION = "2.0.0"
64SDK_REVISION = '639'
65SCRIPT_PATH = path.abspath(path.dirname(__file__))
66sys.path += [SCRIPT_PATH]
67
68VIDEO_MIMETYPE              = 'video/mp4'
69AUDIO_MIMETYPE              = 'audio/mp4'
70SUBTITLES_MIMETYPE          = 'application/mp4'
71VIDEO_DIR                   = 'video'
72AUDIO_DIR                   = 'audio'
73MPD_NS_COMPAT               = 'urn:mpeg:DASH:schema:MPD:2011'
74MPD_NS                      = 'urn:mpeg:dash:schema:mpd:2011'
75SPLIT_INIT_SEGMENT_NAME     = 'init.mp4'
76NOSPLIT_INIT_FILE_PATTERN   = 'init-%s.mp4'
77ONDEMAND_MEDIA_FILE_PATTERN = '%s-%s.mp4'
78
79PADDED_SEGMENT_PATTERN      = 'seg-%05llu.m4s'
80PADDED_SEGMENT_URL_PATTERN  = 'seg-%05d.m4s'
81PADDED_SEGMENT_URL_TEMPLATE = '$RepresentationID$/seg-$Number%05d$.m4s'
82NOPAD_SEGMENT_PATTERN       = 'seg-%llu.m4s'
83NOPAD_SEGMENT_URL_PATTERN   = 'seg-%d.m4s'
84NOPAD_SEGMENT_URL_TEMPLATE  = '$RepresentationID$/seg-$Number$.m4s'
85SEGMENT_PATTERN             = NOPAD_SEGMENT_PATTERN
86SEGMENT_URL_PATTERN         = NOPAD_SEGMENT_URL_PATTERN
87SEGMENT_URL_TEMPLATE        = NOPAD_SEGMENT_URL_TEMPLATE
88
89MEDIA_FILE_PATTERN          = '%s-%02d.mp4'
90
91MARLIN_SCHEME_ID_URI        = 'urn:uuid:5E629AF5-38DA-4063-8977-97FFBD9902D4'
92MARLIN_MAS_NAMESPACE        = 'urn:marlin:mas:1-0:services:schemas:mpd'
93MARLIN_PSSH_SYSTEM_ID       = '69f908af481646ea910ccd5dcccb0a3a'
94
95PLAYREADY_PSSH_SYSTEM_ID    = '9a04f07998404286ab92e65be0885f95'
96PLAYREADY_SCHEME_ID_URI     = 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95'
97PLAYREADY_SCHEME_ID_URI_V10 = 'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95'
98PLAYREADY_MSPR_NAMESPACE    = 'urn:microsoft:playready'
99
100WIDEVINE_PSSH_SYSTEM_ID     = 'edef8ba979d64acea3c827dcd51d21ed'
101WIDEVINE_SCHEME_ID_URI      = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
102
103PRIMETIME_PSSH_SYSTEM_ID    = 'f239e769efa348509c16a903c6932efb'
104PRIMETIME_SCHEME_ID_URI     = 'urn:uuid:F239E769-EFA3-4850-9C16-A903C6932EFB'
105
106MPEG_COMMON_ENCRYPTION_SCHEME_ID_URI = 'urn:mpeg:dash:mp4protection:2011'
107
108EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID = '1077efecc0b24d02ace33c1e52e2fb4b'
109EME_COMMON_ENCRYPTION_SCHEME_ID_URI  = 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b'
110
111CLEARKEY_CONTENT_PROTECTION_VALUE = 'ClearKey1.0'
112CLEARKEY_SCHEME_ID_URI            = 'urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e'
113CLEARKEY_LICENSE_TYPE             = 'EME-1.0'
114CLEARKEY_CKEY_NAMESPACE           = 'http://dashif.org/guidelines/clearKey'
115
116SMOOTH_DEFAULT_TIMESCALE    = 10000000
117
118SMIL_NAMESPACE              = 'http://www.w3.org/2001/SMIL20/Language'
119
120CENC_2013_NAMESPACE         = 'urn:mpeg:cenc:2013'
121
122DASHIF_NAMESPACE            = 'https://dashif.org/'
123
124DASH_DEFAULT_ROLE_NAMESPACE = 'urn:mpeg:dash:role:2011'
125
126DASH_MEDIA_SEGMENT_URL_PATTERN_SMOOTH = "/QualityLevels($Bandwidth$)/Fragments(%s=$Time$)"
127DASH_MEDIA_SEGMENT_URL_PATTERN_HIPPO  = '%s/Bitrate($Bandwidth$)/Fragment($Time$)'
128
129HIPPO_MEDIA_SEGMENT_REGEXP_DEFAULT = '%s/Bitrate\\\\(%d\\\\)/Fragment\\\\((\\\\d+)\\\\)'
130HIPPO_MEDIA_SEGMENT_GROUPS_DEFAULT = '["time"]'
131HIPPO_MEDIA_SEGMENT_REGEXP_SMOOTH  = 'QualityLevels\\\\(%d\\\\)/Fragments\\\\(%s=(\\\\d+)\\\\)'
132HIPPO_MEDIA_SEGMENT_GROUPS_SMOOTH  = '["time"]'
133
134MPEG_DASH_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI       = 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011'
135ISO_IEC_23001_8_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI = 'urn:mpeg:mpegB:cicp:ChannelConfiguration'
136DOLBY_DIGITAL_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI   = 'tag:dolby.com,2014:dash:audio_channel_configuration:2011'
137DOLBY_AC4_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI       = 'tag:dolby.com,2015:dash:audio_channel_configuration:2015'
138
139ISOFF_MAIN_PROFILE          = 'urn:mpeg:dash:profile:isoff-main:2011'
140ISOFF_LIVE_PROFILE          = 'urn:mpeg:dash:profile:isoff-live:2011'
141ISOFF_ON_DEMAND_PROFILE     = 'urn:mpeg:dash:profile:isoff-on-demand:2011'
142HBBTV_15_ISOFF_LIVE_PROFILE = 'urn:hbbtv:dash:profile:isoff-live:2012'
143ProfileAliases = {
144  'main':      ISOFF_MAIN_PROFILE,
145  'live':      ISOFF_LIVE_PROFILE,
146  'on-demand': ISOFF_ON_DEMAND_PROFILE,
147  'hbbtv-1.5': HBBTV_15_ISOFF_LIVE_PROFILE
148}
149
150TempFiles = []
151
152MpegCencSchemeMap = {
153    'cenc': 'MPEG-CENC',
154    'cbc1': 'MPEG-CBC1',
155    'cens': 'MPEG-CENS',
156    'cbcs': 'MPEG-CBCS'
157}
158
159#############################################
160def AddSegmentList(options, container, subdir, track, use_byte_range=False):
161    if subdir:
162        prefix = subdir+'/'
163    else:
164        prefix = ''
165    segment_list = xml.SubElement(container,
166                                  'SegmentList',
167                                  timescale='1000',
168                                  duration=str(int(round(track.average_segment_duration*1000))))
169    if use_byte_range:
170        byte_range = str(track.parent.init_segment.position)+'-'+str(track.parent.init_segment.position+track.parent.init_segment.size-1)
171        xml.SubElement(segment_list,
172                       'Initialization',
173                       sourceURL=prefix + track.parent.media_name,
174                       range=byte_range)
175    else:
176        xml.SubElement(segment_list,
177                       'Initialization',
178                       sourceURL=prefix + track.init_segment_name)
179    i = 1
180    for segment_index in track.moofs:
181        segment = track.parent.segments[segment_index]
182        segment_offset = segment[0].position
183        segment_length = reduce(operator.add, [atom.size for atom in segment], 0)
184        if use_byte_range:
185            byte_range = str(segment_offset) + '-' + str(segment_offset + segment_length - 1)
186            xml.SubElement(segment_list,
187                           'SegmentURL',
188                           media=prefix + track.parent.media_name,
189                           mediaRange=byte_range)
190        else:
191            xml.SubElement(segment_list,
192                           'SegmentURL',
193                           media=prefix + (SEGMENT_URL_PATTERN % i))
194        i += 1
195
196
197#############################################
198def AddSegmentTemplate(options, container, init_segment_url, media_url_template_prefix, track, stream_name):
199    if options.use_segment_list:
200        return
201
202    if options.use_segment_timeline or track.type == 'subtitles':
203        url_template = SEGMENT_URL_TEMPLATE
204        if options.smooth:
205            url_base = path.basename(options.smooth_server_manifest_filename)
206            url_template = url_base + DASH_MEDIA_SEGMENT_URL_PATTERN_SMOOTH % stream_name
207        elif options.hippo:
208            url_template = DASH_MEDIA_SEGMENT_URL_PATTERN_HIPPO % stream_name
209
210        args = [container, 'SegmentTemplate']
211        kwargs = {'timescale': str(track.timescale),
212                  'initialization': init_segment_url,
213                  'media': url_template,
214                  'startNumber': '1'} # (keep the @startNumber, even if not needed, because some clients like Silverlight want it)
215
216        segment_template = xml.SubElement(*args, **kwargs)
217        segment_timeline = xml.SubElement(segment_template, 'SegmentTimeline')
218        repeat_count = 0
219        for i in range(len(track.segment_scaled_durations)):
220            duration = track.segment_scaled_durations[i]
221            if i + 1 < len(track.segment_scaled_durations) and duration == track.segment_scaled_durations[i + 1]:
222                repeat_count += 1
223            else:
224                args = [segment_timeline, 'S']
225                kwargs = {'d': str(duration)}
226                if repeat_count:
227                    kwargs['r'] = str(repeat_count)
228
229                xml.SubElement(*args, **kwargs)
230                repeat_count = 0
231    else:
232        xml.SubElement(container,
233                       'SegmentTemplate',
234                       timescale='1000',
235                       duration=str(int(round(track.average_segment_duration*1000))),
236                       initialization=init_segment_url,
237                       media=SEGMENT_URL_TEMPLATE,
238                       startNumber='1') # (keep the @startNumber, even if not needed, because some clients like Silverlight want it)
239
240#############################################
241def AddSegments(options, container, track):
242    if options.use_segment_list:
243        if options.split:
244            subdir = track.representation_id
245            use_byte_range = False
246        else:
247            subdir = None
248            use_byte_range = True
249        AddSegmentList(options, container, subdir, track, use_byte_range)
250
251#############################################
252def AddContentProtection(options, container, tracks, all_tracks):
253    kids = []
254    for track in tracks:
255        kid = track.key_info.get('kid')
256        if kid != None and kid not in kids:
257            kids.append(kid)
258    if not kids:
259        return # nothing encrypted here
260
261    # resolve the default KID and KEY
262    key_info = None
263    if tracks:
264        key_info = tracks[0].key_info
265
266    if key_info:
267        default_kid = key_info['kid']
268        default_key = key_info.get('key', None)
269    else:
270        default_kid = kids[0]
271        default_key = None
272
273    # compute a list of all keys if we need to merge keys into a single list
274    if options.merge_keys:
275        key_set = GetKeySet(all_tracks)
276        kids    = [x[0] for x in key_set]
277    else:
278        key_set = [(default_kid, default_key)]
279
280    # EME Common Encryption
281    if options.eme_signaling in ['pssh-v0', 'pssh-v1']:
282        container.append(xml.Comment(' EME Common Encryption '))
283        xml.register_namespace('cenc', CENC_2013_NAMESPACE)
284        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=EME_COMMON_ENCRYPTION_SCHEME_ID_URI, value=options.encryption_cenc_scheme)
285        if options.eme_signaling == 'pssh-v1':
286            pssh_box = MakePsshBoxV1(bytes.fromhex(EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID), kids, b'')
287        else:
288            pssh_box = MakePsshBox(bytes.fromhex(EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID), b'')
289        pssh_b64 = Base64Encode(pssh_box)
290        pssh = xml.SubElement(cp, '{' + CENC_2013_NAMESPACE + '}pssh')
291        pssh.text = pssh_b64
292
293    # MPEG Common Encryption
294    container.append(xml.Comment(' MPEG Common Encryption '))
295    xml.register_namespace('cenc', CENC_2013_NAMESPACE)
296    cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=MPEG_COMMON_ENCRYPTION_SCHEME_ID_URI, value=options.encryption_cenc_scheme)
297    default_kid_with_dashes = (default_kid[0:8]+'-'+default_kid[8:12]+'-'+default_kid[12:16]+'-'+default_kid[16:20]+'-'+default_kid[20:32]).lower()
298    cp.set('{'+CENC_2013_NAMESPACE+'}default_KID', default_kid_with_dashes)
299
300    # Clearkey
301    if options.clearkey:
302        container.append(xml.Comment(' Clear Key '))
303        xml.register_namespace('ckey', CLEARKEY_CKEY_NAMESPACE)
304        xml.register_namespace('dashif', DASHIF_NAMESPACE)
305        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=CLEARKEY_SCHEME_ID_URI, value=CLEARKEY_CONTENT_PROTECTION_VALUE)
306        if options.clearkey_license_uri:
307            # First entry with the legacy namespace
308            ck_license = xml.SubElement(cp, '{'+CLEARKEY_CKEY_NAMESPACE+'}Laurl', Lic_type=CLEARKEY_LICENSE_TYPE)
309            ck_license.text = options.clearkey_license_uri
310
311            # Second entry with the dashif namespace
312            ck_license = xml.SubElement(cp, '{'+DASHIF_NAMESPACE+'}laurl')
313            ck_license.text = options.clearkey_license_uri
314
315    # Marlin
316    if options.marlin:
317        container.append(xml.Comment(' Marlin '))
318        xml.register_namespace('mas', MARLIN_MAS_NAMESPACE)
319        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=MARLIN_SCHEME_ID_URI)
320        cids = xml.SubElement(cp, '{' + MARLIN_MAS_NAMESPACE + '}MarlinContentIds')
321        for kid in kids:
322            cid = xml.SubElement(cids, '{' + MARLIN_MAS_NAMESPACE + '}MarlinContentId')
323            cid.text = 'urn:marlin:kid:' + kid
324
325    # PlayReady
326    if options.playready:
327        container.append(xml.Comment(' PlayReady '))
328        xml.register_namespace('mspr', PLAYREADY_MSPR_NAMESPACE)
329        if HBBTV_15_ISOFF_LIVE_PROFILE in options.profiles:
330            playread_scheme_id_uri = PLAYREADY_SCHEME_ID_URI_V10
331        else:
332            playread_scheme_id_uri = PLAYREADY_SCHEME_ID_URI
333        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=playread_scheme_id_uri, value="2.0")
334
335        header_bin = ComputePlayReadyHeader(options.playready_version,
336                                            options.playready_header,
337                                            options.encryption_cenc_scheme,
338                                            key_set)
339        header_b64 = Base64Encode(header_bin)
340        pro = xml.SubElement(cp, '{' + PLAYREADY_MSPR_NAMESPACE + '}pro')
341        pro.text = header_b64
342        pssh_box = MakePsshBox(bytes.fromhex(PLAYREADY_PSSH_SYSTEM_ID), header_bin)
343        pssh_b64 = Base64Encode(pssh_box)
344        pssh = xml.SubElement(cp, '{' + CENC_2013_NAMESPACE + '}pssh')
345        pssh.text = pssh_b64
346
347    # Widevine
348    if options.widevine:
349        container.append(xml.Comment(' Widevine '))
350        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=WIDEVINE_SCHEME_ID_URI)
351        if options.widevine_header:
352            pssh_box = ComputeWidevinePssh(options.widevine_header, options.encryption_cenc_scheme, default_kid)
353            pssh_b64 = Base64Encode(pssh_box)
354            pssh = xml.SubElement(cp, '{' + CENC_2013_NAMESPACE + '}pssh')
355            pssh.text = pssh_b64
356
357    # Primetime
358    if options.primetime:
359        container.append(xml.Comment(' Primetime '))
360        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=PRIMETIME_SCHEME_ID_URI)
361        if options.primetime_metadata:
362            pssh_payload = ComputePrimetimeMetaData(options.primetime_metadata, default_kid)
363            pssh_box = MakePsshBox(bytes.fromhex(PRIMETIME_PSSH_SYSTEM_ID), pssh_payload)
364            pssh_b64 = Base64Encode(pssh_box)
365            pssh = xml.SubElement(cp, '{' + CENC_2013_NAMESPACE + '}pssh')
366            pssh.text = pssh_b64
367
368#############################################
369def AddDescriptor(adaptation_set, set_attributes, set_name, category_name):
370    attributes = set_attributes.get(set_name)
371    if not attributes and category_name:
372        # try a catch-all category set name
373        attributes = set_attributes.get(category_name)
374        if attributes:
375            set_name = category_name
376    if not attributes:
377        return
378
379    for descriptor_name in attributes:
380        descriptor_values = attributes[descriptor_name]
381        for descriptor_value in descriptor_values.split(','):
382            descriptor_namespace = None
383
384            if descriptor_name.lower() == 'role':
385                descriptor_namespace = DASH_DEFAULT_ROLE_NAMESPACE
386            elif descriptor_name.startswith('{'):
387                closeparen = descriptor_name.find('}')
388                if closeparen >= 0:
389                    descriptor_namespace = descriptor_name[1:closeparen]
390                    descriptor_name = descriptor_name[closeparen+1:]
391
392            # support using lowercase names instead of capitalized names
393            if descriptor_name == 'accessibility': descriptor_name = 'Accessibility'
394            if descriptor_name == 'role':          descriptor_name = 'Role'
395            if descriptor_name == 'rating':        descriptor_name = 'Rating'
396            if descriptor_name == 'viewpoint':     descriptor_name = 'Viewpoint'
397
398            if descriptor_name not in ['Accessibility', 'Role', 'Rating', 'Viewpoint']:
399                continue
400            if descriptor_namespace:
401                xml.SubElement(adaptation_set,
402                               descriptor_name,
403                               schemeIdUri=descriptor_namespace,
404                               value=descriptor_value)
405            else:
406                sys.stderr.write('WARNING: ignoring ' + descriptor_name + ' descriptor for set "' + set_name + '", the schemeIdUri must be specified\n')
407
408#############################################
409def OutputDash(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files):
410    all_audio_tracks     = sum(list(audio_sets.values()),     [])
411    all_video_tracks     = sum(list(video_sets.values()),     [])
412    all_subtitles_tracks = sum(list(subtitles_sets.values()), [])
413
414    # compute the total duration (we take the duration of the video)
415    if all_video_tracks:
416        presentation_duration = all_video_tracks[0].total_duration
417    elif all_audio_tracks:
418        presentation_duration = all_audio_tracks[0].total_duration
419    elif all_subtitles_tracks:
420        presentation_duration = all_subtitles_tracks[0].total_duration
421    else:
422        return
423
424    # create the MPD
425    if options.use_compat_namespace:
426        mpd_ns = MPD_NS_COMPAT
427    else:
428        mpd_ns = MPD_NS
429    mpd = xml.Element('MPD',
430                      xmlns=mpd_ns,
431                      profiles=','.join(options.profiles),
432                      minBufferTime="PT%.02fS" % options.min_buffer_time,
433                      mediaPresentationDuration=XmlDuration(presentation_duration),
434                      type='static')
435    mpd.append(xml.Comment(' Created with Bento4 mp4-dash.py, VERSION=' + VERSION + '-' + SDK_REVISION + ' '))
436    period = xml.SubElement(mpd, 'Period')
437
438    # process the video tracks
439    if video_sets:
440        period.append(xml.Comment(' Video '))
441
442        for video_tracks in list(video_sets.values()):
443            # compute the max values
444            maxWidth  = 0
445            maxHeight = 0
446            for video_track in video_tracks:
447                if video_track.width  > maxWidth:  maxWidth  = video_track.width
448                if video_track.height > maxHeight: maxHeight = video_track.height
449
450            adaptation_set = xml.SubElement(period,
451                                            'AdaptationSet',
452                                            mimeType=VIDEO_MIMETYPE,
453                                            segmentAlignment='true',
454                                            startWithSAP='1',
455                                            maxWidth=str(maxWidth),
456                                            maxHeight=str(maxHeight))
457
458            # see if we have descriptors
459            AddDescriptor(adaptation_set, set_attributes, 'video', None)
460
461            # setup content protection
462            if options.encryption_key or options.marlin or options.playready or options.widevine:
463                AddContentProtection(options, adaptation_set, video_tracks, all_audio_tracks + all_video_tracks)
464
465            if options.on_demand:
466                adaptation_set.set('subsegmentAlignment', 'true')
467                adaptation_set.set('subsegmentStartsWithSAP', '1')
468            else:
469                if options.split:
470                    init_segment_url                  = '$RepresentationID$/' + SPLIT_INIT_SEGMENT_NAME
471                    media_segment_url_template_prefix = '$RepresentationID$/'
472                else:
473                    init_segment_url                  = NOSPLIT_INIT_FILE_PATTERN % ('$RepresentationID$')
474                    media_segment_url_template_prefix = ''
475                AddSegmentTemplate(options, adaptation_set, init_segment_url, media_segment_url_template_prefix, video_tracks[0], 'video')
476
477            for video_track in video_tracks:
478                representation = xml.SubElement(adaptation_set,
479                                                'Representation',
480                                                id=video_track.representation_id,
481                                                codecs=video_track.codec,
482                                                width=str(video_track.width),
483                                                height=str(video_track.height),
484                                                scanType=video_track.scan_type,
485                                                frameRate=video_track.frame_rate_ratio,
486                                                bandwidth=str(video_track.bandwidth))
487                if hasattr(video_track, 'max_playout_rate'):
488                    representation.set('maxPlayoutRate', video_track.max_playout_rate)
489
490                if options.on_demand:
491                    base_url = xml.SubElement(representation, 'BaseURL')
492                    base_url.text = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, video_track.representation_id)
493                    sidx_range = (video_track.sidx_atom.position, video_track.sidx_atom.position+video_track.sidx_atom.size-1)
494                    init_range = (0, video_track.moov_atom.position+video_track.moov_atom.size-1)
495                    segment_base = xml.SubElement(representation, 'SegmentBase', indexRange=str(sidx_range[0])+'-'+str(sidx_range[1]))
496                    xml.SubElement(segment_base, 'Initialization', range=str(init_range[0])+'-'+str(init_range[1]))
497                else:
498                    AddSegments(options, representation, video_track)
499
500    # process the audio tracks
501    if audio_sets:
502        period.append(xml.Comment(' Audio '))
503        for _, audio_tracks in list(audio_sets.items()):
504            args = [period, 'AdaptationSet']
505            kwargs = {'mimeType': AUDIO_MIMETYPE, 'startWithSAP': '1', 'segmentAlignment': 'true'}
506            language = audio_tracks[0].language
507            if (language != 'und') or options.always_output_lang:
508                kwargs['lang'] = language
509            label = audio_tracks[0].label
510            if label != '':
511                kwargs['label'] = label
512            adaptation_set = xml.SubElement(*args, **kwargs)
513
514            # see if we have descriptors
515            AddDescriptor(adaptation_set, set_attributes, 'audio/' + language, 'audio')
516
517            # setup content protection
518            if options.encryption_key or options.marlin or options.playready or options.widevine:
519                AddContentProtection(options, adaptation_set, audio_tracks, all_audio_tracks + all_video_tracks)
520
521            if options.on_demand:
522                adaptation_set.set('subsegmentAlignment', 'true')
523                adaptation_set.set('subsegmentStartsWithSAP', '1')
524            else:
525                if options.split:
526                    init_segment_url                  = '$RepresentationID$/' + SPLIT_INIT_SEGMENT_NAME
527                    media_segment_url_template_prefix = '$RepresentationID$/'
528                else:
529                    init_segment_url                  = NOSPLIT_INIT_FILE_PATTERN % ('$RepresentationID$')
530                    media_segment_url_template_prefix = ''
531
532                stream_name = 'audio_' + language
533                AddSegmentTemplate(options, adaptation_set, init_segment_url, media_segment_url_template_prefix, audio_tracks[0], stream_name)
534
535            for audio_track in audio_tracks:
536                representation = xml.SubElement(adaptation_set,
537                                                'Representation',
538                                                id=audio_track.representation_id,
539                                                codecs=audio_track.codec,
540                                                bandwidth=str(audio_track.bandwidth),
541                                                audioSamplingRate=str(audio_track.sample_rate))
542                if audio_track.codec == 'ec-3':
543                    audio_channel_config_value = ComputeDolbyDigitalPlusAudioChannelConfig(audio_track)
544                    scheme_id_uri = DOLBY_DIGITAL_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI
545                elif audio_track.codec.startswith('ac-4'):
546                    audio_channel_config_value = ComputeDolbyAc4AudioChannelConfig(audio_track)
547                    scheme_id_uri = DOLBY_AC4_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI
548                else:
549                    # detect the actual number of channels
550                    sample_description = audio_track.info['sample_descriptions'][0]
551                    if 'mpeg_4_audio_decoder_config' in sample_description:
552                        audio_channel_config_value = str(sample_description['mpeg_4_audio_decoder_config']['channels'])
553                    else:
554                        audio_channel_config_value = str(audio_track.channels)
555                    scheme_id_uri = MPEG_DASH_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI if options.use_legacy_audio_channel_config_uri else ISO_IEC_23001_8_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI
556                xml.SubElement(representation,
557                               'AudioChannelConfiguration',
558                               schemeIdUri=scheme_id_uri,
559                               value=audio_channel_config_value)
560
561                if options.on_demand:
562                    base_url = xml.SubElement(representation, 'BaseURL')
563                    base_url.text = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, audio_track.representation_id)
564                    sidx_range = (audio_track.sidx_atom.position, audio_track.sidx_atom.position+audio_track.sidx_atom.size-1)
565                    init_range = (0, audio_track.moov_atom.position+audio_track.moov_atom.size-1)
566                    segment_base = xml.SubElement(representation, 'SegmentBase', indexRange=str(sidx_range[0])+'-'+str(sidx_range[1]))
567                    xml.SubElement(segment_base, 'Initialization', range=str(init_range[0])+'-'+str(init_range[1]))
568                else:
569                    AddSegments(options, representation, audio_track)
570
571    # process all the subtitles tracks
572    if subtitles_sets:
573        period.append(xml.Comment(' Subtitles (Encapsulated) '))
574        for _, subtitles_tracks in list(subtitles_sets.items()):
575            for subtitles_track in subtitles_tracks:
576                args = [period, 'AdaptationSet']
577                kwargs = {'mimeType': SUBTITLES_MIMETYPE, 'startWithSAP': '1'}
578                if (subtitles_track.language != 'und') or options.always_output_lang:
579                    kwargs['lang'] = subtitles_track.language
580                adaptation_set = xml.SubElement(*args, **kwargs)
581
582                # add a 'subtitles' role
583                xml.SubElement(adaptation_set, 'Role', schemeIdUri='urn:mpeg:dash:role:2011', value='subtitle')
584
585                # see if we have other descriptors
586                AddDescriptor(adaptation_set, set_attributes, 'subtitles/' + subtitles_track.language, 'subtitles')
587
588                representation = xml.SubElement(adaptation_set,
589                                                'Representation',
590                                                id=subtitles_track.representation_id,
591                                                codecs=subtitles_track.codec,
592                                                bandwidth=str(subtitles_track.bandwidth))
593
594                if options.on_demand:
595                    base_url = xml.SubElement(representation, 'BaseURL')
596                    base_url.text = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, subtitles_track.representation_id)
597                    sidx_range = (subtitles_track.sidx_atom.position, subtitles_track.sidx_atom.position+subtitles_track.sidx_atom.size-1)
598                    init_range = (0, subtitles_track.moov_atom.position+subtitles_track.moov_atom.size-1)
599                    segment_base = xml.SubElement(representation, 'SegmentBase', indexRange=str(sidx_range[0])+'-'+str(sidx_range[1]))
600                    xml.SubElement(segment_base, 'Initialization', range=str(init_range[0])+'-'+str(init_range[1]))
601                else:
602                    if options.split:
603                        init_segment_url                  = '$RepresentationID$/' + SPLIT_INIT_SEGMENT_NAME
604                        media_segment_url_template_prefix = '$RepresentationID$/'
605                    else:
606                        init_segment_url                  = NOSPLIT_INIT_FILE_PATTERN % ('$RepresentationID$')
607                        media_segment_url_template_prefix = ''
608
609                    stream_name = 'subtitles_' + subtitles_track.language
610                    AddSegmentTemplate(options, adaptation_set, init_segment_url, media_segment_url_template_prefix, subtitles_track, stream_name)
611                    AddSegments(options, representation, subtitles_track)
612
613    # process all the subtitles files
614    if subtitles_files:
615        period.append(xml.Comment(' Subtitles (Sidecar) '))
616        for subtitles_file in subtitles_files:
617            args = [period, 'AdaptationSet']
618            kwargs = {
619                'mimeType':    subtitles_file.mime_type,
620                'contentType': 'text'
621            }
622            if (subtitles_file.language != None):
623                kwargs['lang'] = subtitles_file.language
624            adaptation_set = xml.SubElement(*args, **kwargs)
625
626            # add a 'subtitles' role
627            xml.SubElement(adaptation_set, 'Role', schemeIdUri='urn:mpeg:dash:role:2011', value='subtitle')
628
629            # see if we have other descriptors
630            AddDescriptor(adaptation_set, set_attributes, 'subtitles/' + subtitles_file.language, 'subtitles')
631
632            # estimate the bandwidth
633            bandwidth = 1024 # default
634            if presentation_duration and subtitles_file.size:
635                bandwidth = int((8*subtitles_file.size)/presentation_duration)
636
637            representation = xml.SubElement(adaptation_set,
638                                            'Representation',
639                                            id='subtitles/'+subtitles_file.language,
640                                            bandwidth=str(bandwidth))
641            base_url = xml.SubElement(representation, 'BaseURL')
642            base_url.text = 'subtitles/'+subtitles_file.language+'/'+subtitles_file.media_name
643
644    # save the MPD
645    if options.mpd_filename:
646        mpd_xml = parseString(xml.tostring(mpd)).toprettyxml("  ")
647        # use a regex to fix a bug in toprettyxml() that inserts newlines in text content
648        mpd_xml = re.sub(r'((?<=>)(\n[\s]*)(?=[^<\s]))|(?<=[^>\s])(\n[\s]*)(?=<)', '', mpd_xml)
649        open(path.join(options.output_dir, options.mpd_filename), 'w').write(mpd_xml)
650
651
652#############################################
653def ComputeHlsWidevineKeyLine(options, track):
654    # V2 key line
655    kid = track.key_info['kid']
656    pssh_box = ComputeWidevinePssh(options.widevine_header, options.encryption_cenc_scheme, kid)
657    key_line = 'URI="data:text/plain;base64,{}",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",KEYID=0x{},KEYFORMATVERSIONS="1"'.format(
658        Base64Encode(pssh_box),
659        kid
660    )
661
662    return key_line
663
664#############################################
665def ComputeHlsPlayReadyKeyLine(options, track, all_tracks):
666    # compute a list of all keys if we need to merge keys into a single list
667    if options.merge_keys:
668        key_set = GetKeySet(all_tracks)
669    else:
670        key_set = [(track.key_info.get('kid'), track.key_info.get('key'))]
671
672    pr_header = ComputePlayReadyHeader(options.playready_version,
673                                       options.playready_header,
674                                       options.encryption_cenc_scheme,
675                                       key_set)
676    key_line = 'URI="data:text/plain;charset=UTF-16;base64,{}",KEYFORMAT="com.microsoft.playready",KEYFORMATVERSIONS="1"'.format(
677        Base64Encode(pr_header)
678    )
679
680    return key_line
681
682#############################################
683def ComputeHlsFairplayKeyLine(options):
684    return 'URI="'+options.fairplay_key_uri+'",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"'
685
686#############################################
687def OutputHlsCommon(options, track, all_tracks, media_subdir, playlist_name, media_file_name):
688    hls_target_duration = math.ceil(max(track.segment_durations))
689
690    output_dir = path.join(options.output_dir, media_subdir)
691    os.makedirs(output_dir, exist_ok = True)
692    playlist_file = open(path.join(output_dir, playlist_name), 'w', newline='\r\n')
693    playlist_file.write('#EXTM3U\n')
694    playlist_file.write('# Created with Bento4 mp4-dash.py, VERSION=' + VERSION + '-' + SDK_REVISION+'\n')
695    playlist_file.write('#\n')
696    playlist_file.write('#EXT-X-VERSION:6\n')
697    playlist_file.write('#EXT-X-PLAYLIST-TYPE:VOD\n')
698    playlist_file.write('#EXT-X-INDEPENDENT-SEGMENTS\n')
699    playlist_file.write('#EXT-X-TARGETDURATION:{}\n'.format(hls_target_duration))
700    playlist_file.write('#EXT-X-MEDIA-SEQUENCE:0\n')
701    if options.split:
702        playlist_file.write('#EXT-X-MAP:URI="{}"\n'.format(SPLIT_INIT_SEGMENT_NAME))
703    else:
704        init_segment_size = track.parent.init_segment.position + track.parent.init_segment.size
705        playlist_file.write('#EXT-X-MAP:URI="{}",BYTERANGE="{}@0"\n'.format(media_file_name, init_segment_size))
706
707    if options.encryption_key:
708        key_lines = []
709        if options.fairplay_key_uri:
710            key_lines.append(ComputeHlsFairplayKeyLine(options))
711        if options.widevine_header:
712            key_lines.append(ComputeHlsWidevineKeyLine(options, track))
713        if options.playready:
714            key_lines.append(ComputeHlsPlayReadyKeyLine(options, track, all_tracks))
715
716        if not key_lines:
717            key_lines.append('URI="'+options.hls_key_url+'",IV=0x'+track.key_info['iv'])
718
719        for key_line in key_lines:
720            playlist_file.write('#EXT-X-KEY:METHOD=SAMPLE-AES,'+key_line+'\n')
721
722    return playlist_file
723
724#############################################
725def OutputHlsTrack(options, track, all_tracks, media_subdir, media_playlist_name, media_file_name):
726    media_playlist_file = OutputHlsCommon(options, track, all_tracks, media_subdir, media_playlist_name, media_file_name)
727
728    if options.split:
729        segment_pattern = SEGMENT_PATTERN.replace('ll','')
730
731    for i in range(len(track.segment_durations)):
732        media_playlist_file.write('#EXTINF:{},\n'.format(track.segment_durations[i]))
733        if options.on_demand or not options.split:
734            segment          = track.parent.segments[track.moofs[i]]
735            segment_position = segment[0].position
736            segment_size     = reduce(operator.add, [atom.size for atom in segment], 0)
737            media_playlist_file.write('#EXT-X-BYTERANGE:{}@{}\n'.format(segment_size, segment_position))
738            media_playlist_file.write(media_file_name)
739        else:
740            media_playlist_file.write(segment_pattern % (i+1))
741        media_playlist_file.write('\n')
742
743    media_playlist_file.write('#EXT-X-ENDLIST\n')
744
745#############################################
746def OutputHlsWebvttPlaylist(options, media_subdir, media_playlist_name, media_file_name, total_duration):
747    # output a playlist with a single segment that covers the entire WebVTT file
748    output_dir = path.join(options.output_dir, media_subdir)
749    os.makedirs(output_dir, exist_ok = True)
750    playlist_file = open(path.join(output_dir, media_playlist_name), 'w', newline='\r\n')
751    playlist_file.write('#EXTM3U\n')
752    playlist_file.write('# Created with Bento4 mp4-dash.py, VERSION=' + VERSION + '-' + SDK_REVISION+'\n')
753    playlist_file.write('#\n')
754    playlist_file.write('#EXT-X-VERSION:6\n')
755    playlist_file.write('#EXT-X-INDEPENDENT-SEGMENTS\n')
756    playlist_file.write('#EXT-X-PLAYLIST-TYPE:VOD\n')
757    playlist_file.write('#EXT-X-TARGETDURATION:{}\n'.format(total_duration))
758    playlist_file.write('#EXTINF:{},\n'.format(total_duration))
759    playlist_file.write(media_file_name)
760    playlist_file.write('\n')
761    playlist_file.write('#EXT-X-ENDLIST\n')
762
763#############################################
764def OutputHlsIframeIndex(options, track, all_tracks, media_subdir, iframes_playlist_name, media_file_name):
765    index_playlist_file = OutputHlsCommon(options, track, all_tracks, media_subdir, iframes_playlist_name, media_file_name)
766
767    index_playlist_file.write('#EXT-X-I-FRAMES-ONLY\n')
768
769    iframe_total_segment_size = 0
770    iframe_total_segment_duration = 0
771    iframe_bitrate = 0
772    iframe_max_bitrate = 0
773    iframe_average_segment_bitrate = 0
774
775    if not options.split:
776        # get the I-frame index for a single file
777        json_index = Mp4IframeIndex(options, path.join(options.output_dir, media_file_name))
778        index = json.loads(json_index)
779        for i in range(len(track.segment_durations)):
780            if i < len(index):
781                index_entry = index[i]
782                iframe_segment_duration = track.segment_durations[i]
783                index_playlist_file.write('#EXTINF:{},\n'.format(iframe_segment_duration))
784                fragment_start    = int(index_entry['fragmentStart'])
785                iframe_offset     = int(index_entry['offset'])
786                iframe_size       = int(index_entry['size'])
787
788                iframe_total_segment_size += iframe_size
789                iframe_total_segment_duration += iframe_segment_duration
790                iframe_bitrate = 8.0*(iframe_size/iframe_segment_duration)
791                if iframe_bitrate > iframe_max_bitrate:
792                    iframe_max_bitrate = iframe_bitrate
793
794                iframe_range_size = iframe_size + (iframe_offset-fragment_start)
795                index_playlist_file.write('#EXT-X-BYTERANGE:{}@{}\n'.format(iframe_range_size, fragment_start))
796                index_playlist_file.write(media_file_name+'\n')
797    else:
798        segment_pattern = SEGMENT_PATTERN.replace('ll','')
799        for i in range(len(track.segment_durations)):
800            fragment_basename = segment_pattern % (i+1)
801            fragment_file = path.join(options.output_dir, media_subdir, fragment_basename)
802            init_file = path.join(options.output_dir, media_subdir, options.init_segment)
803            if not path.exists(fragment_file):
804                break
805            json_index = Mp4IframeIndex(options, fragment_file, fragments_info=init_file)
806            index = json.loads(json_index)
807            if len(index) < 1:
808                break
809            iframe_size       = int(index[0]['size'])
810            iframe_offset     = int(index[0]['offset'])
811            iframe_range_size = iframe_size + iframe_offset
812            iframe_segment_duration = track.segment_durations[i]
813            index_playlist_file.write('#EXTINF:{},\n'.format(iframe_segment_duration))
814            index_playlist_file.write('#EXT-X-BYTERANGE:{}@0\n'.format(iframe_range_size))
815            index_playlist_file.write(fragment_basename+'\n')
816
817            iframe_total_segment_size += iframe_size
818            iframe_total_segment_duration += iframe_segment_duration
819
820            iframe_bitrate = 8.0*(iframe_size/iframe_segment_duration)
821            if iframe_bitrate > iframe_max_bitrate:
822                iframe_max_bitrate = iframe_bitrate
823
824    index_playlist_file.write('#EXT-X-ENDLIST\n')
825
826    if iframe_total_segment_duration:
827        iframe_average_segment_bitrate = 8.0*(iframe_total_segment_size/iframe_total_segment_duration)
828
829    return (iframe_average_segment_bitrate, iframe_max_bitrate)
830
831#############################################
832def OutputHls(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files):
833    all_audio_tracks     = sum(audio_sets.values(),           [])
834    all_video_tracks     = sum(list(video_sets.values()),     [])
835    all_subtitles_tracks = sum(list(subtitles_sets.values()), [])
836
837    master_playlist_file = open(path.join(options.output_dir, options.hls_master_playlist_name), 'w', newline='\r\n')
838    master_playlist_file.write('#EXTM3U\n')
839    master_playlist_file.write('# Created with Bento4 mp4-dash.py, VERSION=' + VERSION + '-' + SDK_REVISION+'\n')
840    master_playlist_file.write('#\n')
841    master_playlist_file.write('#EXT-X-VERSION:6\n')
842    master_playlist_file.write('\n')
843    master_playlist_file.write('# Media Playlists\n')
844
845    master_playlist_file.write('\n')
846    master_playlist_file.write('# Audio\n')
847
848    # group tracks that don't have an explicit '+hls_group' specifier
849    ungrouped_audio_tracks = [track for track in all_audio_tracks if not track.hls_group]
850    if len(set([track.language for track in ungrouped_audio_tracks])) == 1:
851        # all the tracks have the same language, put them each in a separate group
852        for index, audio_track in enumerate(ungrouped_audio_tracks, start=1):
853            audio_track.hls_group = f'audio/{index}' if len(ungrouped_audio_tracks) > 1 else 'audio'
854    else:
855        # group tracks by codec
856        codec_groups = {}
857        for audio_track in ungrouped_audio_tracks:
858            codec_groups.setdefault(audio_track.codec, []).append(audio_track)
859        for codec, audio_tracks in codec_groups.items():
860            for audio_track in audio_tracks:
861                audio_track.hls_group = f'audio/{codec}'
862
863    # categorize the audio tracks by group
864    audio_groups = {}
865    for audio_track in all_audio_tracks:
866        audio_group_name = audio_track.hls_group
867        audio_group = audio_groups.setdefault(audio_group_name, {
868            'tracks': [],
869            'codecs': set(),
870            'average_segment_bitrate': 0,
871            'max_segment_bitrate': 0
872        })
873        audio_group['tracks'].append(audio_track)
874
875    for audio_group_name, audio_group in audio_groups.items():
876        default_selected = False
877        media_names = []  # used to keep track of all media names used and detect duplicates
878        for audio_track in audio_group['tracks']:
879            # update the average and max bitrates
880            if audio_track.average_segment_bitrate > audio_group['average_segment_bitrate']:
881                audio_group['average_segment_bitrate'] = audio_track.average_segment_bitrate
882            if audio_track.max_segment_bitrate > audio_group['max_segment_bitrate']:
883                audio_group['max_segment_bitrate'] = audio_track.max_segment_bitrate
884
885            # update the codecs
886            audio_group['codecs'].add(audio_track.codec)
887
888            if options.on_demand or not options.split:
889                media_subdir        = ''
890                media_file_name     = audio_track.parent.media_name
891                media_playlist_name = audio_track.representation_id+".m3u8"
892                media_playlist_path = media_playlist_name
893            else:
894                media_subdir        = audio_track.representation_id
895                media_file_name     = ''
896                media_playlist_name = options.hls_media_playlist_name
897                media_playlist_path = media_subdir+'/'+media_playlist_name
898
899            # compute a media name that is unique in the group
900            media_name = audio_track.label if audio_track.label else audio_track.language_name
901            if media_name in media_names:
902                duplicate = media_name
903                for suffix in range(1, len(media_names) + 1):
904                    media_name = f'{duplicate}-{suffix}'
905                    if media_name not in media_names:
906                        break
907            media_names.append(media_name)
908
909            default = audio_track.hls_default
910            if default is None:
911                default = not default_selected
912            if default:
913                default_selected = True
914            master_playlist_file.write('#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="{}",LANGUAGE="{}",NAME="{}",AUTOSELECT={},DEFAULT={},CHANNELS="{}",URI="{}"\n'.format(
915                                       audio_group_name,
916                                       audio_track.language,
917                                       media_name,
918                                       'YES' if audio_track.hls_autoselect else 'NO',
919                                       'YES' if default else 'NO',
920                                       audio_track.channels,
921                                       media_playlist_path))
922            OutputHlsTrack(options, audio_track, all_audio_tracks + all_video_tracks, media_subdir, media_playlist_name, media_file_name)
923
924            # Add an audio stream entry for audio-only presentations or if the track specifiers include a '-' entry
925            # for the group match spec ('+hls_group_match' is equal to or includes the special name '-')
926            if not all_video_tracks or '-' in audio_track.hls_group_match:
927                master_playlist_file.write('#EXT-X-STREAM-INF:AUDIO="{}",AVERAGE-BANDWIDTH={:.0f},BANDWIDTH={:.0f},CODECS="{}"\n'.format(
928                                            audio_group_name,
929                                            audio_track.average_segment_bitrate,
930                                            audio_track.max_segment_bitrate,
931                                            ','.join(audio_group['codecs'])))
932                master_playlist_file.write(media_playlist_path+'\n')
933
934    master_playlist_file.write('\n')
935    master_playlist_file.write('# Video\n')
936    iframe_playlist_lines = []
937    subtitles_group = 'SUBTITLES="subtitles",' if (subtitles_files or all_subtitles_tracks) else ''
938    for video_track in all_video_tracks:
939        if options.on_demand or not options.split:
940            media_subdir          = ''
941            media_file_name       = video_track.parent.media_name
942            media_playlist_name   = video_track.representation_id+".m3u8"
943            media_playlist_path   = media_playlist_name
944            iframes_playlist_name = video_track.representation_id+"_iframes.m3u8"
945            iframes_playlist_path = iframes_playlist_name
946        else:
947            media_subdir          = video_track.representation_id
948            media_file_name       = ''
949            media_playlist_name   = options.hls_media_playlist_name
950            media_playlist_path   = media_subdir+'/'+media_playlist_name
951            iframes_playlist_name = options.hls_iframes_playlist_name
952            iframes_playlist_path = media_subdir+'/'+iframes_playlist_name
953
954        if audio_groups:
955            # one entry per matching audio group
956            for audio_group_name in audio_groups:
957                if '*' not in video_track.hls_group_match and audio_group_name not in video_track.hls_group_match:
958                    continue
959                audio_codecs = ','.join(audio_groups[audio_group_name]['codecs'])
960                master_playlist_file.write('#EXT-X-STREAM-INF:{}AUDIO="{}",AVERAGE-BANDWIDTH={:.0f},BANDWIDTH={:.0f},CODECS="{}",RESOLUTION={:.0f}x{:.0f},FRAME-RATE={:.3f}\n'.format(
961                                           subtitles_group,
962                                           audio_group_name,
963                                           video_track.average_segment_bitrate + audio_groups[audio_group_name]['average_segment_bitrate'],
964                                           video_track.max_segment_bitrate + audio_groups[audio_group_name]['max_segment_bitrate'],
965                                           video_track.codec+','+audio_codecs,
966                                           video_track.width,
967                                           video_track.height,
968                                           video_track.frame_rate))
969                master_playlist_file.write(media_playlist_path+'\n')
970        else:
971            # no audio
972            master_playlist_file.write('#EXT-X-STREAM-INF:{}AVERAGE-BANDWIDTH={:.0f},BANDWIDTH={:.0f},CODECS="{}",RESOLUTION={:.0f}x{:.0f},FRAME-RATE={:.3f}\n'.format(
973                                       subtitles_group,
974                                       video_track.average_segment_bitrate,
975                                       video_track.max_segment_bitrate,
976                                       video_track.codec,
977                                       video_track.width,
978                                       video_track.height,
979                                       video_track.frame_rate))
980            master_playlist_file.write(media_playlist_path+'\n')
981
982        OutputHlsTrack(options, video_track, all_audio_tracks + all_video_tracks, media_subdir, media_playlist_name, media_file_name)
983        iframe_average_segment_bitrate,iframe_max_bitrate = OutputHlsIframeIndex(options, video_track, all_audio_tracks + all_video_tracks, media_subdir, iframes_playlist_name, media_file_name)
984
985        # this will be written later
986        iframe_playlist_lines.append('#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH={:.0f},BANDWIDTH={:.0f},CODECS="{}",RESOLUTION={:.0f}x{:.0f},URI="{}"\n'.format(
987                                     iframe_average_segment_bitrate,
988                                     iframe_max_bitrate,
989                                     video_track.codec,
990                                     video_track.width,
991                                     video_track.height,
992                                     iframes_playlist_path))
993
994    master_playlist_file.write('\n# I-Frame Playlists\n')
995    master_playlist_file.write(''.join(iframe_playlist_lines))
996
997    # IMSC1 subtitles
998    if all_subtitles_tracks:
999        master_playlist_file.write('\n# Subtitles (IMSC1)\n')
1000        default_selected = False
1001        for subtitles_track in all_subtitles_tracks:
1002            if subtitles_track.codec != 'stpp':
1003                # only accept IMSC1 tracks
1004                continue
1005            language = subtitles_track.language
1006            language_name = subtitles_track.language_name
1007
1008            if options.on_demand or not options.split:
1009                media_subdir        = ''
1010                # media_file_name     = subtitles_track.parent.media_name
1011                media_playlist_name = subtitles_track.representation_id+".m3u8"
1012                media_playlist_path = media_playlist_name
1013            else:
1014                media_subdir        = subtitles_track.representation_id
1015                # media_file_name     = ''
1016                media_playlist_name = options.hls_media_playlist_name
1017                media_playlist_path = media_subdir+'/'+media_playlist_name
1018
1019            default = subtitles_track.hls_default and not default_selected
1020            if default is None:
1021                default = not default_selected
1022            if default:
1023                default_selected = True
1024            master_playlist_file.write('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="{}",AUTOSELECT={},DEFAULT={},LANGUAGE="{}",URI="{}"\n'.format(
1025                                       language_name,
1026                                       'YES' if subtitles_track.hls_autoselect else 'NO',
1027                                       'YES' if default else 'NO',
1028                                       language,
1029                                       media_playlist_path))
1030
1031    # WebVTT subtitles
1032    if subtitles_files:
1033        master_playlist_file.write('\n# Subtitles (WebVTT)\n')
1034        presentation_duration = math.ceil(max([track.total_duration for track in all_video_tracks + all_audio_tracks]))
1035        default_selected = False
1036        for subtitles_file in subtitles_files:
1037            media_subdir = 'subtitles/{}'.format(subtitles_file.language)
1038            media_playlist_name = options.hls_media_playlist_name
1039            default = audio_track.hls_default and not default_selected
1040            if default:
1041                default_selected = True
1042            master_playlist_file.write('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="{}",AUTOSELECT={},DEFAULT={},LANGUAGE="{}",URI="{}/{}"\n'.format(
1043                                       language_name,
1044                                       'YES' if subtitles_file.hls_autoselect else 'NO',
1045                                       'YES' if default else 'NO',
1046                                       subtitles_file.language,
1047                                       media_subdir,
1048                                       media_playlist_name))
1049            OutputHlsWebvttPlaylist(options, media_subdir, media_playlist_name, subtitles_file.media_name, presentation_duration)
1050
1051#############################################
1052def OutputSmooth(options, audio_tracks, video_tracks):
1053    # compute the total duration (we take the duration of the video)
1054    if video_tracks:
1055        presentation_duration = video_tracks[0].total_duration
1056    else:
1057        presentation_duration = audio_tracks[0].total_duration
1058
1059    # create the Client Manifest
1060    client_manifest = xml.Element('SmoothStreamingMedia',
1061                                  MajorVersion="2",
1062                                  MinorVersion="0",
1063                                  TimeScale="10000000",
1064                                  Duration=str(int(round(presentation_duration*10000000.0))))
1065    client_manifest.append(xml.Comment(' Created with Bento4 mp4-dash.py, VERSION='+VERSION+'-'+SDK_REVISION+' '))
1066
1067    # process the audio tracks
1068    for audio_track in audio_tracks:
1069        stream_name = audio_track.label
1070        if stream_name == '':
1071            stream_name = audio_track.language_name
1072        if stream_name == '' or stream_name == 'Unknown':
1073            stream_name = "audio_"+audio_track.language
1074        audio_url_pattern="QualityLevels({bitrate})/Fragments(%s={start time})" % (stream_name)
1075        stream_index = xml.SubElement(client_manifest,
1076                                      'StreamIndex',
1077                                      Chunks=str(len(audio_track.moofs)),
1078                                      Url=audio_url_pattern,
1079                                      Type="audio",
1080                                      Name=stream_name,
1081                                      QualityLevels="1",
1082                                      TimeScale=str(audio_track.timescale))
1083        if audio_track.language != 'und' or options.always_output_lang:
1084            stream_index.set('Language', audio_track.language)
1085
1086        if audio_track.codec == 'ec-3':
1087            # Dolby Digital Plus
1088            (channels, codec_private_data) = ComputeDolbyDigitalPlusSmoothStreamingInfo(audio_track)
1089            audio_tag = '65534'
1090            fourcc = 'EC-3'
1091            channels = str(channels)
1092            data_rate = int(audio_track.info['sample_descriptions'][0]['dolby_digital_info']['data_rate'])
1093            packet_size = str(4*data_rate)
1094        else:
1095            # assume AAC
1096            audio_tag = '255'
1097            fourcc = 'AACL'
1098            codec_private_data=audio_track.info['sample_descriptions'][0]['decoder_info']
1099            channels = str(audio_track.channels)
1100            packet_size = str(2*audio_track.channels)
1101
1102        xml.SubElement(stream_index,
1103                       'QualityLevel',
1104                       Bitrate=str(audio_track.bandwidth),
1105                       SamplingRate=str(audio_track.sample_rate),
1106                       Channels=channels,
1107                       BitsPerSample="16",
1108                       PacketSize=packet_size,
1109                       AudioTag=audio_tag,
1110                       FourCC=fourcc,
1111                       Index="0",
1112                       CodecPrivateData=codec_private_data)
1113
1114        for duration in audio_track.segment_scaled_durations:
1115            xml.SubElement(stream_index, "c", d=str(duration))
1116
1117    # process all the video tracks
1118    if video_tracks:
1119        max_width  = max([track.width  for track in video_tracks])
1120        max_height = max([track.height for track in video_tracks])
1121        video_url_pattern="QualityLevels({bitrate})/Fragments(video={start time})"
1122        stream_index = xml.SubElement(client_manifest,
1123                                      'StreamIndex',
1124                                       Chunks=str(len(video_tracks[0].moofs)),
1125                                       Url=video_url_pattern,
1126                                       Type="video",
1127                                       Name="video",
1128                                       QualityLevels=str(len(video_tracks)),
1129                                       TimeScale=str(video_tracks[0].timescale),
1130                                       MaxWidth=str(max_width),
1131                                       MaxHeight=str(max_height))
1132        qindex = 0
1133        for video_track in video_tracks:
1134            sample_desc = video_track.info['sample_descriptions'][0]
1135            codec_private_data = '00000001'+sample_desc['avc_sps'][0]+'00000001'+sample_desc['avc_pps'][0]
1136            xml.SubElement(stream_index,
1137                           'QualityLevel',
1138                           Bitrate=str(video_track.bandwidth),
1139                           MaxWidth=str(video_track.width),
1140                           MaxHeight=str(video_track.height),
1141                           FourCC=options.smooth_h264_fourcc,
1142                           CodecPrivateData=codec_private_data,
1143                           Index=str(qindex))
1144            qindex += 1
1145
1146        for duration in video_tracks[0].segment_scaled_durations:
1147            xml.SubElement(stream_index, "c", d=str(duration))
1148
1149    if options.playready:
1150        if video_tracks:
1151            key_info = video_tracks[0].key_info
1152            if not key_info and len(audio_tracks):
1153                key_info = audio_tracks[0].key_info
1154
1155        if not key_info:
1156            return
1157        kid = key_info.get('kid')
1158        key = key_info.get('key')
1159        header_bin = ComputePlayReadyHeader(options.playready_version,
1160                                            options.playready_header,
1161                                            options.encryption_cenc_scheme,
1162                                            [(kid, key)])
1163        header_b64 = Base64Encode(header_bin)
1164        protection = xml.SubElement(client_manifest, 'Protection')
1165        protection_header = xml.SubElement(protection,
1166                                           'ProtectionHeader',
1167                                           SystemID='9a04f079-9840-4286-ab92-e65be0885f95')
1168        protection_header.text = header_b64
1169
1170    # save the Smooth Client Manifest
1171    if options.smooth_client_manifest_filename != '':
1172        open(path.join(options.output_dir, options.smooth_client_manifest_filename), 'w').write(parseString(xml.tostring(client_manifest)).toprettyxml('  '))
1173
1174    # create the Server Manifest file
1175    server_manifest = xml.Element('smil', xmlns=SMIL_NAMESPACE)
1176    server_manifest_head = xml.SubElement(server_manifest, 'head')
1177    xml.SubElement(server_manifest_head,
1178                   'meta',
1179                   name='clientManifestRelativePath',
1180                   content=path.basename(options.smooth_client_manifest_filename))
1181    server_manifest_body = xml.SubElement(server_manifest, 'body')
1182    server_manifest_switch = xml.SubElement(server_manifest_body, 'switch')
1183    for audio_track in audio_tracks:
1184        audio_entry = xml.SubElement(server_manifest_switch,
1185                                     'audio',
1186                                     src=audio_track.parent.media_name,
1187                                     systemBitrate=str(audio_track.bandwidth))
1188        xml.SubElement(audio_entry,
1189                       'param',
1190                       name='trackID',
1191                       value=str(audio_track.id),
1192                       valueType='data')
1193        if audio_track.language:
1194            xml.SubElement(audio_entry,
1195                           'param',
1196                           name='trackName',
1197                           value="audio_" + audio_track.language,
1198                           valueType='data')
1199        if audio_track.timescale != SMOOTH_DEFAULT_TIMESCALE:
1200            xml.SubElement(audio_entry,
1201                           'param',
1202                           name='timeScale',
1203                           value=str(audio_track.timescale),
1204                           valueType='data')
1205
1206    for video_track in video_tracks:
1207        video_entry = xml.SubElement(server_manifest_switch,
1208                                     'video',
1209                                     src=video_track.parent.media_name,
1210                                     systemBitrate=str(video_track.bandwidth))
1211        xml.SubElement(video_entry,
1212                       'param',
1213                       name='trackID',
1214                       value=str(video_track.id),
1215                       valueType='data')
1216        if video_track.timescale != SMOOTH_DEFAULT_TIMESCALE:
1217            xml.SubElement(video_entry,
1218                           'param',
1219                           name='timeScale',
1220                           value=str(video_track.timescale),
1221                           valueType='data')
1222
1223    # save the Manifest
1224    if options.smooth_server_manifest_filename != '':
1225        open(path.join(options.output_dir, options.smooth_server_manifest_filename), 'w').write(parseString(xml.tostring(server_manifest)).toprettyxml('  '))
1226
1227#############################################
1228def OutputHippo(options, audio_tracks, video_tracks):
1229    # create the Server Manifest file
1230    server_manifest = '{\n  "media": [\n'
1231    sep = ''
1232    for track in audio_tracks+video_tracks:
1233        server_manifest += sep+'    {\n'
1234        server_manifest += '      "trackId": '+ str(track.id) + ',\n'
1235        server_manifest += '      "mediaSegments": {\n'
1236        server_manifest += '        "urls": [\n'
1237        server_manifest += '          {\n'
1238        if options.smooth:
1239            server_manifest += '            "pattern": "'+(HIPPO_MEDIA_SEGMENT_REGEXP_SMOOTH % (track.bandwidth, track.stream_id))+'",\n'
1240            server_manifest += '            "fields": '+HIPPO_MEDIA_SEGMENT_GROUPS_SMOOTH+'\n'
1241        else:
1242            server_manifest += '            "pattern": "'+(HIPPO_MEDIA_SEGMENT_REGEXP_DEFAULT % (track.stream_id, track.bandwidth))+'",\n'
1243            server_manifest += '            "fields": '+HIPPO_MEDIA_SEGMENT_GROUPS_DEFAULT+'\n'
1244        server_manifest += '          }\n'
1245        server_manifest += '        ],\n'
1246        server_manifest += '        "file": "' + track.parent.media_name + '"\n'
1247        server_manifest += '      },\n'
1248        server_manifest += '      "initSegment": {\n'
1249        server_manifest += '        "file": "' + track.init_segment_name + '"\n'
1250        server_manifest += '      }\n'
1251        server_manifest += '    }'
1252        sep = ',\n'
1253    server_manifest += '\n  ]\n}'
1254
1255    # save the Manifest
1256    if options.hippo_server_manifest_filename != '':
1257        open(path.join(options.output_dir, options.hippo_server_manifest_filename), 'w').write(server_manifest)
1258
1259#############################################
1260def SelectTracks(options, media_sources):
1261    # parse the media files
1262    file_list_index = 1
1263    mp4_files = {}
1264    mp4_media_names = []
1265    for media_source in [x for x in media_sources if x.format == 'mp4']:
1266        media_file = media_source.filename
1267
1268        # check if we have already parsed this file
1269        if media_file in mp4_files:
1270            media_source.mp4_file = mp4_files[media_file]
1271            continue
1272
1273        # parse the file
1274        if not path.exists(media_file):
1275            PrintErrorAndExit('ERROR: media file ' + media_file + ' does not exist')
1276
1277        # get the file info
1278        print('Parsing media file', str(file_list_index)+':', GetMappedFileName(media_file))
1279        mp4_file = Mp4File(options, media_source)
1280
1281        # set some metadata properties for this file
1282        mp4_file.file_list_index = file_list_index
1283        file_list_index += 1
1284        if options.rename_media:
1285            mp4_file.media_name = MEDIA_FILE_PATTERN % (options.media_prefix, mp4_file.file_list_index)
1286        elif '+media' in media_source.spec:
1287            mp4_file.media_name = media_source.spec['+media']
1288        elif 'media' in media_source.spec: # we still accept the form without the '+' sign, for legacy support
1289            mp4_file.media_name = media_source.spec['media']
1290        else:
1291            mp4_file.media_name = path.basename(media_source.original_filename)
1292
1293        if not options.split:
1294            if mp4_file.media_name in mp4_media_names:
1295                PrintErrorAndExit('ERROR: output media name {} is not unique, consider using --rename-media'.format(mp4_file.media_name))
1296
1297        # check the file
1298        if mp4_file.info['movie']['fragments'] != True:
1299            PrintErrorAndExit('ERROR: file '+str(mp4_file.file_list_index)+' is not fragmented (use mp4fragment to fragment it)')
1300
1301        # set the source property
1302        media_source.mp4_file = mp4_file
1303        mp4_files[media_file] = mp4_file
1304        mp4_media_names.append(mp4_file.media_name)
1305
1306    # select tracks
1307    audio_adaptation_sets     = {}
1308    video_adaptation_sets     = {}
1309    subtitles_adaptation_sets = {}
1310    label_indexes             = {}
1311    for media_source in media_sources:
1312        track_id       = media_source.spec['track']
1313        track_type     = media_source.spec['type']
1314        track_language = media_source.spec['language']
1315        tracks         = []
1316
1317        if media_source.format != 'mp4':
1318            if track_id or track_type:
1319                PrintErrorAndExit('ERROR: track ID and track type selections only apply to MP4 media sources')
1320
1321            continue
1322
1323        if track_type not in ['', 'audio', 'video', 'subtitles']:
1324            sys.stderr.write('WARNING: ignoring source '+media_source.name+', unknown type')
1325
1326        if track_id and track_type:
1327            PrintErrorAndExit('ERROR: track ID and track type selections are mutually exclusive')
1328
1329        if track_id:
1330            tracks = [media_source.mp4_file.find_track_by_id(track_id)]
1331            if not tracks:
1332                PrintErrorAndExit('ERROR: track id not found for media file '+media_source.name)
1333
1334        if track_type:
1335            tracks = media_source.mp4_file.find_tracks_by_type(track_type)
1336            if not tracks:
1337                PrintErrorAndExit('ERROR: no ' + track_type + ' found for media file '+media_source.name)
1338
1339        if not tracks:
1340            tracks = list(media_source.mp4_file.tracks.values())
1341
1342        # pre-process the track metadata
1343        for track in tracks:
1344            # track language
1345            language = LanguageCodeMap.get(track.language, track.language)
1346            if track_language and track_language != language and track_language != track.language:
1347                continue
1348            remap_language = media_source.spec.get('+language')
1349            if remap_language:
1350                language = remap_language
1351            elif options.language_map and language in options.language_map:
1352                language = options.language_map[language]
1353            track.language = language
1354
1355            # track language name
1356            track.language_name = media_source.spec.get('+language_name', LanguageNames.get(language, language))
1357
1358            # track representation id
1359            custom_representation_id = media_source.spec.get('+representation_id')
1360            if custom_representation_id:
1361                track.representation_id = custom_representation_id
1362
1363            # video scan type
1364            if track.type == 'video':
1365                track.scan_type = media_source.spec.get('+scan_type', track.scan_type)
1366
1367            # label
1368            track.label = media_source.spec.get('+label', '')
1369
1370            # HLS options
1371            track.hls_default = media_source.spec.get('+hls_default', None)  # None means: unspecified
1372            if track.hls_default is not None:
1373                track.hls_default = BooleanFromString(track.hls_default)
1374            track.hls_autoselect = BooleanFromString(media_source.spec.get('+hls_autoselect', 'YES'))
1375            track.hls_group = media_source.spec.get('+hls_group')
1376            track.hls_group_match = media_source.spec.get('+hls_group_match', '*').split('&')
1377
1378        # update label indexes (so that we can use numbers instead of strings for labels)
1379        for track in tracks:
1380            if track.label not in label_indexes:
1381                label_indexes[track.label] = len(label_indexes) + 1
1382
1383        # process audio tracks
1384        for track in [t for t in tracks if t.type == 'audio']:
1385            adaptation_set_name = ('audio', track.language, track.codec)
1386
1387            # add the label index
1388            adaptation_set_name += (str(label_indexes[track.label]),)
1389
1390            # lookup the adaptation set and start a new one if no entry is found
1391            adaptation_set = audio_adaptation_sets.get(adaptation_set_name, [])
1392
1393            # only keep this track if there isn't already a track with the same
1394            # codec at the same bitrate (within 10%)
1395            with_same_bandwidth = [t for t in adaptation_set if abs(float(t.bandwidth-track.bandwidth)/float(t.bandwidth)) < 0.1]
1396            if with_same_bandwidth:
1397                continue
1398
1399            audio_adaptation_sets[adaptation_set_name] = adaptation_set
1400            adaptation_set.append(track)
1401            track.order_index = len(adaptation_set)
1402
1403        # process video tracks
1404        for track in [t for t in tracks if t.type == 'video']:
1405            adaptation_set_name = ('video', track.codec_family)
1406            adaptation_set = video_adaptation_sets.get(adaptation_set_name, [])
1407            video_adaptation_sets[adaptation_set_name] = adaptation_set
1408            adaptation_set.append(track)
1409            track.order_index = len(adaptation_set)
1410
1411        # process subtitle tracks
1412        if options.subtitles:
1413            for track in [t for t in tracks if t.type == 'subtitles']:
1414                adaptation_set_name = ('subtitles', track.language, track.codec_family)
1415                adaptation_set = subtitles_adaptation_sets.get(adaptation_set_name, [])
1416                subtitles_adaptation_sets[adaptation_set_name] = adaptation_set
1417
1418                if [et for et in adaptation_set if et.language == track.language]:
1419                    continue # only accept one track for each language
1420
1421                adaptation_set.append(track)
1422                track.order_index = len(adaptation_set)
1423
1424    # Try to simplify the adaptation set names where there's unnecessary sub-classification
1425    # NOTE: in this version, we only try to simplify based on the last element of the name
1426    # and we only try to simplify audio adaptation sets, because they are the only ones with
1427    # labels. This should be updated when/if we add support for labels on other types.
1428    for adaptation_set in [audio_adaptation_sets]:
1429        prefixed = {}
1430
1431        # skip empty sets
1432        if not adaptation_set:
1433            continue
1434
1435        for name in adaptation_set:
1436            prefix = name[:-1]
1437            entry = prefixed.get(name[:-1], [0, name])
1438            entry[0] += 1
1439            prefixed[prefix] = entry
1440
1441        for prefix in prefixed:
1442            [count, name] = prefixed[prefix]
1443            if count == 1:
1444                # there's only one entry with that prefix, we can simplify
1445                adaptation_set[prefix] = adaptation_set[name]
1446                del adaptation_set[name]
1447
1448    return (audio_adaptation_sets, video_adaptation_sets, subtitles_adaptation_sets, mp4_files)
1449
1450#############################################
1451def SelectSubtitlesFiles(options, media_sources):
1452    return [SubtitlesFile(options, media_source) for media_source in media_sources if media_source.format in ['ttml', 'webvtt']]
1453
1454#############################################
1455def KeySpecToKeyInfo(options, key_spec):
1456    key_info = {'filter': ['audio', 'video']}
1457    if key_spec.startswith('audio:') or key_spec.startswith('video:'):
1458        separator = key_spec.find(':')
1459        key_info['filter'] = [key_spec[:separator]]
1460        key_spec = key_spec[separator+1:]
1461    iv_hex = None
1462    if key_spec.startswith('@'):
1463        kid_hex, key_hex = GetEncryptionKey(options, key_spec[1:])
1464    else:
1465        if ':' not in key_spec:
1466            raise Exception('Invalid argument syntax for --encryption-key option')
1467        parts = key_spec.split(':', 2)
1468        kid_hex = parts[0]
1469        key_hex = parts[1]
1470        if len(parts) > 2:
1471            iv_hex = parts[2]
1472        if len(kid_hex) != 32:
1473            raise Exception('Invalid argument format for --encryption-key option')
1474
1475        if key_hex.startswith('#'):
1476            if len(key_hex) != 41:
1477                raise Exception('Invalid argument format for --encryption-key option')
1478            key_seed_bin = Base64Decode(key_hex[1:])
1479            kid_bin = bytes.fromhex(kid_hex)
1480            key_hex = DerivePlayReadyKey(key_seed_bin, kid_bin).hex()
1481            if options.verbose:
1482                print('PlayReady Derived Key =', key_hex)
1483        else:
1484            if len(key_hex) != 32:
1485                raise Exception('Invalid argument format for --encryption-key option')
1486
1487    key_info['key'] = key_hex
1488    key_info['kid'] = kid_hex
1489    key_info['iv']  = iv_hex or 'random'
1490    if options.hls and not iv_hex:
1491        # for HLS, we need to know the IV
1492        import random
1493        sys_random = random.SystemRandom()
1494        random_iv = sys_random.getrandbits(128)
1495        key_info['iv'] = '%016x' % random_iv
1496
1497    return key_info
1498
1499#############################################
1500def ResolveEncryptionKeys(options):
1501    for key_spec in options.encryption_key.split(','):
1502        options.key_infos.append(KeySpecToKeyInfo(options, key_spec))
1503
1504#############################################
1505# Compute a list of all unique KIDs and associated keys for a list of tracks
1506#############################################
1507def GetKeySet(tracks):
1508    key_sets = {}
1509    for track in [x for x in tracks if 'kid' in x.key_info]:
1510        key_sets[track.key_info['kid']] = (track.key_info['kid'], track.key_info.get('key'))
1511    return list(key_sets.values())
1512
1513#############################################
1514def PrepareSources(options, media_sources):
1515    for media_source in [x for x in media_sources if x.format == 'mp4']:
1516        key_infos = options.key_infos[:] # make a copy of the shared key infos
1517
1518        # check if there's a key override for this media source,
1519        # if we find one, that will take precendence over the global
1520        # key infos
1521        key_spec = media_source.spec.get('+key')
1522        if key_spec:
1523            key_infos.append(KeySpecToKeyInfo(options, key_spec))
1524
1525        # select which KID/KEY to use for each track
1526        for track in media_source.mp4_info['tracks']:
1527            for key_info in key_infos:
1528                if track['type'].lower() in key_info['filter']:
1529                    media_source.key_infos[track['id']] = key_info
1530
1531#############################################
1532def EncryptSources(options, media_sources):
1533    # check if there's anything to encrypt
1534    if not options.encryption_key:
1535        found_key_spec = False
1536        for media_source in media_sources:
1537            if media_source.spec.get('+key'):
1538                found_key_spec
1539                break
1540        if not found_key_spec:
1541            # nothing to encrypt
1542            return
1543
1544    encrypted_files = {}
1545    for media_source in [x for x in media_sources if x.format == 'mp4']:
1546        media_file = media_source.filename
1547
1548        # check if we have already encrypted this file
1549        if media_file in encrypted_files:
1550            media_source.filename = encrypted_files[media_file].name
1551            continue
1552
1553        if not media_source.mp4_info['movie']['fragments']:
1554            PrintErrorAndExit('ERROR: file ' + media_file + ' is not fragmented (use mp4fragment to fragment it)')
1555
1556        if not len(media_source.mp4_info['tracks']):
1557            raise Exception('No track found in input file(s)')
1558
1559        # skip now if we're only outputing the MPD
1560        if options.no_media:
1561            continue
1562
1563        # don't process any further if we won't have key material for this media source
1564        if not media_source.key_infos:
1565            continue
1566
1567        # pick a default key
1568        default_kid = options.key_infos[0]['kid']
1569
1570        print('Encrypting track IDs ' + str(sorted(media_source.key_infos.keys()) ) +' in ' + GetMappedFileName(media_file))
1571        encrypted_file = tempfile.NamedTemporaryFile(dir=options.output_dir, delete=False)
1572        encrypted_files[media_file] = encrypted_file
1573        TempFiles.append(encrypted_file.name)
1574        encrypted_file.close() # necessary on Windows
1575        MapFileName(encrypted_file.name, path.basename(encrypted_file.name) + ' = Encrypted[' + GetMappedFileName(media_file) + ']')
1576        args = ['--method', MpegCencSchemeMap[options.encryption_cenc_scheme]]
1577
1578        if options.encryption_args:
1579            args += options.encryption_args.split()
1580        else:
1581            if options.smooth or options.playready:
1582                args += ['--global-option', 'mpeg-cenc.piff-compatible:true']
1583
1584        key_set = {}
1585        for track_id in sorted(media_source.key_infos.keys()):
1586            key_info = media_source.key_infos[track_id]
1587            key_set[key_info['kid']] = key_info['key']
1588            args += ['--key', str(track_id)+':'+key_info['key']+':'+key_info['iv'], '--property', str(track_id)+':KID:'+key_info['kid']]
1589
1590        # EME Common Encryption / Clearkey
1591        if options.eme_signaling == 'pssh-v0':
1592            args += ['--pssh', EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID+':']
1593        elif options.eme_signaling == 'pssh-v1':
1594            args += ['--pssh-v1', EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID+':']
1595
1596        # Marlin
1597        if options.marlin_add_pssh:
1598            marlin_pssh = ComputeMarlinPssh(options)
1599            pssh_file = tempfile.NamedTemporaryFile(dir=options.output_dir, delete=False)
1600            pssh_file.write(marlin_pssh)
1601            TempFiles.append(pssh_file.name)
1602            pssh_file.close() # necessary on Windows
1603            args += ['--pssh', MARLIN_PSSH_SYSTEM_ID+':'+pssh_file.name]
1604
1605        # PlayReady
1606        if options.playready_add_pssh:
1607            playready_header = ComputePlayReadyHeader(options.playready_version,
1608                                                      options.playready_header,
1609                                                      options.encryption_cenc_scheme,
1610                                                      list(key_set.items()))
1611            pssh_file = tempfile.NamedTemporaryFile(dir=options.output_dir, delete=False)
1612            pssh_file.write(playready_header)
1613            TempFiles.append(pssh_file.name)
1614            pssh_file.close() # necessary on Windows
1615            args += ['--pssh', PLAYREADY_PSSH_SYSTEM_ID+':'+pssh_file.name]
1616
1617        # Widevine
1618        if options.widevine_header:
1619            pssh = ComputeWidevinePssh(options.widevine_header, options.encryption_cenc_scheme, default_kid)
1620            pssh_version = pssh[8]
1621            if pssh_version == 0:
1622                pssh_payload_offset = 32
1623            elif pssh_version == 1:
1624                kid_count = struct.unpack('>I', pssh[28:32])[0]
1625                pssh_payload_offset = 32 + (16 * kid_count) + 4
1626                if pssh_payload_offset > len(pssh):
1627                    raise Exception('invalid pssh format')
1628            else:
1629                raise Exception('pssh version > 1 is not supported')
1630            pssh_payload = pssh[pssh_payload_offset:]
1631            pssh_file = tempfile.NamedTemporaryFile(dir=options.output_dir, delete=False)
1632            pssh_file.write(pssh_payload)
1633            TempFiles.append(pssh_file.name)
1634            pssh_file.close() # necessary on Windows
1635            args += ['--pssh' if pssh_version == 0 else '--pssh-v1', WIDEVINE_PSSH_SYSTEM_ID+':'+pssh_file.name]
1636
1637        # Primetime
1638        if options.primetime_metadata:
1639            primetime_metadata = ComputePrimetimeMetaData(options.primetime_metadata, default_kid)
1640            pssh_file = tempfile.NamedTemporaryFile(dir=options.output_dir, delete=False)
1641            pssh_file.write(primetime_metadata)
1642            TempFiles.append(pssh_file.name)
1643            pssh_file.close() # necessary on Windows
1644            args += ['--pssh', PRIMETIME_PSSH_SYSTEM_ID+':'+pssh_file.name]
1645
1646        Mp4Encrypt(options, media_file, encrypted_file.name, *args)
1647        media_source.filename = encrypted_file.name
1648
1649#############################################
1650def ComputeWidevinePssh(header_spec, encryption_scheme, kid):
1651    if header_spec.startswith('#'):
1652        # The header spec is a base-64 encoded precomputed byte array
1653        header = Base64Decode(header_spec[1:])
1654        if not header:
1655            raise Exception('invalid base64 encoding')
1656
1657        # The header may be a raw header or a full PSSH box, find out which
1658        if len(header) > 8:
1659            (box_length, box_type) = struct.unpack('>I4s', header[:8])
1660            if box_length == len(header) and box_type == b'pssh':
1661                # That looks like a valid PSSH box
1662                return header
1663
1664    else:
1665        # The header spec is a set of properties
1666        header = ComputeWidevineHeader(header_spec, encryption_scheme, kid)
1667
1668    # Wrap the header in a PSSH box
1669    return MakePsshBox(bytes.fromhex(WIDEVINE_PSSH_SYSTEM_ID), header)
1670
1671#############################################
1672FileNameMap = {}
1673def MapFileName(from_name, to_name):
1674    global FileNameMap
1675    FileNameMap[from_name] = to_name
1676
1677def GetMappedFileName(filename):
1678    return FileNameMap.get(filename, filename)
1679
1680#############################################
1681Options = None
1682def main():
1683    # determine the platform binary name
1684    host_platform = ''
1685    if platform.system() == 'Linux':
1686        if platform.processor() == 'x86_64':
1687            host_platform = 'linux-x86_64'
1688        else:
1689            host_platform = 'linux-x86'
1690    elif platform.system() == 'Darwin':
1691        host_platform = 'macosx'
1692    elif platform.system() == 'Windows':
1693        host_platform = 'win32'
1694    default_exec_dir = path.join(SCRIPT_PATH, 'bin', host_platform)
1695    if not path.exists(default_exec_dir):
1696        default_exec_dir = path.join(SCRIPT_PATH, 'bin')
1697    if not path.exists(default_exec_dir):
1698        default_exec_dir = path.join(SCRIPT_PATH, '..', 'bin')
1699    if not path.exists(default_exec_dir):
1700        default_exec_dir = '-'
1701
1702    # parse options
1703    parser = OptionParser(usage="%prog [options] <media-file> [<media-file> ...]",
1704                          description="Each <media-file> is the path to a fragmented MP4 file, optionally prefixed with a stream selector delimited by [ and ]. The same input MP4 file may be repeated, provided that the stream selector prefixes select different streams. Version " + VERSION + " r" + SDK_REVISION)
1705    parser.add_option('-v', '--verbose', dest="verbose", action='store_true', default=False,
1706                      help="Be verbose")
1707    parser.add_option('-d', '--debug', dest="debug", action='store_true', default=False,
1708                      help="Print out debugging information")
1709    parser.add_option('-o', '--output-dir', dest="output_dir",
1710                      help="Output directory", metavar="<output-dir>", default='output')
1711    parser.add_option('-f', '--force', dest="force_output", action="store_true",
1712                      help="Allow output to an existing directory", default=False)
1713    parser.add_option('', '--mpd-name', dest="mpd_filename",
1714                      help="MPD file name", metavar="<filename>", default='stream.mpd')
1715    parser.add_option('', '--profiles', dest='profiles',
1716                      help="Comma-separated list of one or more profile(s). Complete profile names can be used, or profile aliases ('live'='"+ISOFF_LIVE_PROFILE+"', 'on-demand'='"+ISOFF_ON_DEMAND_PROFILE+"', 'hbbtv-1.5='"+HBBTV_15_ISOFF_LIVE_PROFILE+"')", default='live',
1717                      metavar="<profiles>")
1718    parser.add_option('', '--no-media', dest="no_media", action='store_true', default=False,
1719                      help="Do not output media files (MPD/Manifests only)")
1720    parser.add_option('', '--rename-media', dest='rename_media', action='store_true', default=False,
1721                      help = 'Use a file name pattern instead of the base name of input files for output media files.')
1722    parser.add_option('', '--media-prefix', dest='media_prefix', metavar='<prefix>', default='media',
1723                      help='Use this prefix for prefixed media file names (instead of the default prefix "media")')
1724    parser.add_option('', '--init-segment', dest="init_segment",
1725                      help="Initialization segment name", metavar="<filename>", default='init.mp4')
1726    parser.add_option('', "--no-split", action="store_false", dest="split", default=True,
1727                      help="Do not split the file into individual segment files")
1728    parser.add_option('', "--use-segment-list", action="store_true", dest="use_segment_list", default=False,
1729                      help="Use segment lists instead of segment templates")
1730    parser.add_option('', '--use-segment-template-number-padding', action='store_true', dest='segment_template_padding', default=False,
1731                      help="Use padded numbers in segment URL/filename templates")
1732    parser.add_option('', "--use-segment-timeline", action="store_true", dest="use_segment_timeline", default=False,
1733                      help="Use segment timelines (necessary if segment durations vary)")
1734    parser.add_option('', "--min-buffer-time", metavar='<duration>', dest="min_buffer_time", type="float", default=0.0,
1735                      help="Minimum buffer time (in seconds)")
1736    parser.add_option('', "--max-playout-rate", metavar='<strategy>', dest='max_playout_rate_strategy',
1737                      help="Max Playout Rate setting strategy for trick-play support. Supported strategies: lowest:X"),
1738    parser.add_option('', "--language-map", dest="language_map", metavar="<lang_from>:<lang_to>[,...]",
1739                      help="Remap language code <lang_from> to <lang_to>. Multiple mappings can be specified, separated by ','")
1740    parser.add_option('', "--always-output-lang", dest="always_output_lang", action='store_true', default=False,
1741                      help="Always output an @lang attribute for audio tracks even when the language is undefined"),
1742    parser.add_option('', "--subtitles", dest="subtitles", action="store_true", default=False,
1743                      help="Enable Subtitles")
1744    parser.add_option('', "--attributes", dest="attributes", action="append", metavar='<attributes-definition>', default=[],
1745                      help="Specify the attributes of a set of tracks. This option may be used multiple times, once per attribute set.")
1746    parser.add_option('', "--smooth", dest="smooth", default=False, action="store_true",
1747                      help="Produce an output compatible with Smooth Streaming")
1748    parser.add_option('', '--smooth-client-manifest-name', dest="smooth_client_manifest_filename",
1749                      help="Smooth Streaming Client Manifest file name", metavar="<filename>", default='stream.ismc')
1750    parser.add_option('', '--smooth-server-manifest-name', dest="smooth_server_manifest_filename",
1751                      help="Smooth Streaming Server Manifest file name", metavar="<filename>", default='stream.ism')
1752    parser.add_option('', '--smooth-h264-fourcc', dest='smooth_h264_fourcc',
1753                      help="Smooth Streaming FourCC value for H.264 video (default=H264) [some older players use AVC1]", metavar="<fourcc>", default='H264')
1754    parser.add_option('', '--hls', dest="hls", default=False, action="store_true",
1755                      help="Output HLS playlists in addition to MPEG DASH")
1756    parser.add_option('', '--hls-key-url', dest="hls_key_url",
1757                      help="HLS key URL (default: key.bin)", metavar="<url>", default='key.bin')
1758    parser.add_option('', '--hls-master-playlist-name', dest="hls_master_playlist_name",
1759                      help="HLS master playlist name (default: master.m3u8)", metavar="<filename>", default='master.m3u8')
1760    parser.add_option('', '--hls-media-playlist-name', dest="hls_media_playlist_name",
1761                      help="HLS media playlist name (default: media.m3u8)", metavar="<filename>", default='media.m3u8')
1762    parser.add_option('', '--hls-iframes-playlist-name', dest="hls_iframes_playlist_name",
1763                      help="HLS I-Frames playlist name (default: iframes.m3u8)", metavar="<filename>", default='iframes.m3u8')
1764    parser.add_option('', "--hippo", dest="hippo", default=False, action="store_true",
1765                      help="Produce an output compatible with the Hippo Media Server")
1766    parser.add_option('', '--hippo-server-manifest-name', dest="hippo_server_manifest_filename",
1767                      help="Hippo Media Server Manifest file name", metavar="<filename>", default='stream.msm')
1768    parser.add_option('', "--use-compat-namespace", dest="use_compat_namespace", action="store_true", default=False,
1769                      help="Use the original DASH MPD namespace as it was specified in the first published specification")
1770    parser.add_option('', "--use-legacy-audio-channel-config-uri", dest="use_legacy_audio_channel_config_uri", action="store_true", default=False,
1771                      help="Use the legacy DASH namespace URI for the AudioChannelConfiguration descriptor")
1772    parser.add_option('', "--encryption-key", dest="encryption_key", metavar='<key-spec>', default=None,
1773                      help="Encrypt some or all tracks with MPEG CENC (AES-128), where <key-spec> specifies the KID(s) and Key(s) to use, using one of the following forms: " +
1774                           "(1) <KID>:<key> or <KID>:<key>:<IV> with <KID> (and <IV> if specififed) as a 32-character hex string and <key> either a 32-character hex string or the character '#' followed by a base64-encoded key seed; or " +
1775                           "(2) @<key-locator> where <key-locator> is an expression of one of the supported key locator schemes. Each entry may be prefixed with an optional track filter, and multiple <key-spec> entries can be used, separated by ','. (see online docs for details)")
1776    parser.add_option('', "--encryption-cenc-scheme", dest="encryption_cenc_scheme", metavar='<cenc-scheme>', default='cenc', choices=('cenc', 'cbc1', 'cens', 'cbcs'),
1777                      help="MPEG Common Encryption scheme (cenc, cbc1, cens or cbcs). (default: cenc)")
1778    parser.add_option('', "--encryption-args", dest="encryption_args", metavar='<cmdline-arguments>', default=None,
1779                      help="Pass additional command line arguments to mp4encrypt (separated by spaces)")
1780    parser.add_option('', "--eme-signaling", dest="eme_signaling", metavar='<eme-signaling-type>', choices=['pssh-v0', 'pssh-v1'],
1781                      help="Add EME-compliant signaling in the MPD and PSSH boxes (valid options are 'pssh-v0' and 'pssh-v1')")
1782    parser.add_option('', "--merge-keys", dest="merge_keys", action="store_true", default=False,
1783                      help="Merge all keys in a single set used for all <ContentProtection> elements")
1784    parser.add_option('', "--marlin", dest="marlin", action="store_true", default=False,
1785                      help="Add Marlin signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
1786    parser.add_option('', "--marlin-add-pssh", dest="marlin_add_pssh", action="store_true", default=False,
1787                      help="Add an (optional) Marlin 'pssh' box in the init segment(s)")
1788    parser.add_option('', "--playready", dest="playready", action="store_true", default=False,
1789                      help="Add PlayReady signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
1790    parser.add_option('', "--playready-version", dest="playready_version", choices=("4.0", "4.1", "4.2", "4.3"), default="4.0",
1791                      help="PlayReady version to use (4.0, 4.1, 4.2, 4.3), defaults to 4.0")
1792    parser.add_option('', "--playready-header", dest="playready_header", metavar='<playready-header>', default=None,
1793                      help="Add a PlayReady PRO element in the MPD and a PlayReady PSSH box in the init segments. The use of this option implies the --playready option. " +
1794                           "The <playready-header> argument can be either: " +
1795                           "(1) the character '@' followed by the name of a file containing a PlayReady XML Rights Management Header (<WRMHEADER>) or a PlayReady Header Object (PRO) in binary form,  or "
1796                           "(2) the character '#' followed by a PlayReady Header Object encoded in Base64, or " +
1797                           "(3) one or more <name>:<value> pair(s) (separated by '#' if more than one) specifying fields of a PlayReady Header Object (field names include LA_URL, LUI_URL and DS_ID)")
1798    parser.add_option('', "--playready-add-pssh", dest="playready_add_pssh", action="store_true", default=False,
1799                      help="Store the PlayReady header in a 'pssh' box in the init segment(s) [deprecated: this is now implicitly on by default when the --playready or --playready-header option is used]")
1800    parser.add_option('', "--playready-no-pssh", dest="playready_no_pssh", action="store_true", default=False,
1801                      help="Do not store the PlayReady header in a 'pssh' box in the init segment(s)")
1802    parser.add_option('', "--widevine", dest="widevine", action="store_true", default=False,
1803                      help="Add Widevine signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
1804    parser.add_option('', "--widevine-header", dest="widevine_header", metavar='<widevine-header>', default=None,
1805                      help="Add a Widevine entry in the MPD, and a Widevine PSSH box in the init segments. The use of this option implies the --widevine option. " +
1806                           "The <widevine-header> argument can be either: " +
1807                           "(1) the character '#' followed by a Widevine header encoded in Base64 (either a complete PSSH box or just the PSSH box payload), or " +
1808                           "(2) one or more <name>:<value> pair(s) (separated by '#' if more than one) specifying fields of a Widevine header (field names include 'provider' [string], 'content_id' [byte array in hex], 'policy' [string])")
1809    parser.add_option('', "--primetime", dest="primetime", action="store_true", default=False,
1810                      help="Add Primetime signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
1811    parser.add_option('', "--primetime-metadata", dest="primetime_metadata", metavar='<primetime-metadata>', default=None,
1812                      help="Add Primetime metadata in a PSSH box in the init segments. The use of this option implies the --primetime option. " +
1813                           "The <primetime-data> argument can be either: " +
1814                           "(1) the character '@' followed by the name of a file containing the Primetime Metadata to use, or "
1815                           "(2) the character '#' followed by the Primetime Metadata encoded in Base64")
1816    parser.add_option('', "--fairplay-key-uri", dest="fairplay_key_uri",
1817                      help="Specify the key URI to use for FairPlay Streaming key delivery (only valid with --hls option)")
1818    parser.add_option('', "--clearkey", dest="clearkey", action="store_true",
1819                      help="Add Clear Key signaling to the MPD (requires an encrypted input, or the --encryption-key option))")
1820    parser.add_option('', "--clearkey-license-uri", dest="clearkey_license_uri",
1821                      help="Specify the license/key URI to use for Clear Key (only valid with --clearkey option)")
1822    parser.add_option('', "--exec-dir", metavar="<exec_dir>", dest="exec_dir", default=default_exec_dir,
1823                      help="Directory where the Bento4 executables are located (use '-' to look for executable in the current PATH)")
1824    (options, args) = parser.parse_args()
1825    if not args:
1826        parser.print_help()
1827        sys.exit(1)
1828    global Options
1829    Options = options
1830
1831    # set some synthetic (not from command line) options
1832    options.on_demand = False
1833    options.key_infos = []
1834
1835    # check the consistency of the options
1836    if options.smooth:
1837        options.split = False
1838        options.use_segment_timeline = True
1839        if options.use_segment_list:
1840            raise Exception('ERROR: --smooth and --use-segment-list are mutually exclusive')
1841
1842    if options.hippo:
1843        options.split = False
1844        options.use_segment_timeline = True
1845        if options.use_segment_list:
1846            raise Exception('ERROR: --hippo and --use-segment-list are mutually exclusive')
1847
1848    if options.exec_dir != "-":
1849        if not path.exists(options.exec_dir):
1850            PrintErrorAndExit('Executable directory does not exist ('+Options.exec_dir+'), use --exec-dir')
1851
1852    if options.max_playout_rate_strategy:
1853        if not options.max_playout_rate_strategy.startswith('lowest:'):
1854            PrintErrorAndExit('Max Playout Rate strategy '+options.max_playout_rate_strategy+' is not supported')
1855
1856    # switch variables
1857    if options.segment_template_padding:
1858        global SEGMENT_PATTERN, SEGMENT_URL_PATTERN, SEGMENT_URL_TEMPLATE
1859        SEGMENT_PATTERN      = PADDED_SEGMENT_PATTERN
1860        SEGMENT_URL_PATTERN  = PADDED_SEGMENT_URL_PATTERN
1861        SEGMENT_URL_TEMPLATE = PADDED_SEGMENT_URL_TEMPLATE
1862
1863    # post-process some of the options
1864    if not options.profiles:
1865        if options.use_segment_list:
1866            options.profiles = [ISOFF_MAIN_PROFILE]
1867        else:
1868            options.profiles = [ISOFF_LIVE_PROFILE]
1869    else:
1870        profiles = []
1871        for profile in options.profiles.split(','):
1872            profile = profile.strip()
1873            if profile in ProfileAliases:
1874                profile = ProfileAliases[profile]
1875            profiles.append(profile)
1876        options.profiles = profiles
1877    if ISOFF_ON_DEMAND_PROFILE in options.profiles:
1878        options.on_demand = True
1879        options.split = False
1880        if ISOFF_LIVE_PROFILE in options.profiles:
1881            raise Exception('on-demand and live profiles are mutually exclusive')
1882    if HBBTV_15_ISOFF_LIVE_PROFILE in options.profiles:
1883        options.playready_no_pssh = True
1884        if options.playready_add_pssh:
1885            sys.stderr.write('INFO: since hbbtv-1.5 profile is selected, no PlayReady PSSH box will be added to the init segments\n')
1886        options.always_output_lang = True
1887
1888    if not options.split:
1889        if not options.smooth and not options.hippo and not options.on_demand and not options.use_segment_list:
1890            sys.stderr.write('WARNING: --no-split requires --use-segment-list, which will be enabled automatically\n')
1891            options.use_segment_list = True
1892
1893    if options.on_demand and options.use_segment_list:
1894        raise Exception('segment lists cannot be used with the on-demand profile')
1895
1896    if options.smooth:
1897        if ISOFF_LIVE_PROFILE not in options.profiles:
1898            raise Exception('--smooth requires the live profile')
1899
1900    if options.hippo:
1901        if ISOFF_LIVE_PROFILE not in options.profiles:
1902            raise Exception('--hippo requires the live profile')
1903
1904    if options.verbose:
1905        print('Profiles:', ','.join(options.profiles))
1906
1907    if options.playready_header or options.playready_add_pssh:
1908        options.playready = True
1909
1910    if options.playready:
1911        options.playready_add_pssh = True
1912
1913    if options.playready_no_pssh:
1914        options.playready_add_pssh = False
1915
1916    if options.widevine_header:
1917        options.widevine = True
1918
1919    if options.primetime_metadata:
1920        options.primetime = True
1921
1922    if options.fairplay_key_uri:
1923        if not options.hls:
1924            sys.stderr.write('WARNING: --fairplay-key-uri is only valid with --hls, ignoring\n')
1925
1926    if options.clearkey_license_uri:
1927        if not options.clearkey:
1928            sys.stderr.write('WARNING: --clearkey-license-uri is only valid with --clearkey, ignoring\n')
1929
1930    if options.hls:
1931        if options.encryption_key and options.encryption_cenc_scheme != 'cbcs':
1932            raise Exception('--hls requires --encryption-cenc-scheme=cbcs')
1933
1934    # process language map options
1935    if options.language_map:
1936        mappings = options.language_map.split(',')
1937        options.language_map = {}
1938        for mapping in mappings:
1939            from_lang, to_lang = mapping.split(':')
1940            options.language_map[from_lang] = to_lang
1941
1942    # parse the attributes definitions
1943    set_attributes = {}
1944    for set_attributes_spec in options.attributes:
1945        try:
1946            set_name, attributes = set_attributes_spec.split(':', 1)
1947            set_attributes[set_name] = {}
1948            for attribute in attributes.split(','):
1949                name, value = attribute.split('=', 1)
1950                set_attributes[set_name][name] = value
1951        except:
1952            raise Exception('Invalid syntax for --attributes option')
1953
1954    # create the output directory
1955    severity = 'ERROR'
1956    if options.no_media: severity = 'WARNING'
1957    if options.force_output: severity = None
1958    MakeNewDir(dir=options.output_dir, exit_if_exists = not (options.no_media or options.force_output), severity=severity)
1959
1960    # parse media sources syntax
1961    media_sources = [MediaSource(options, source) for source in args]
1962
1963    # for on-demand, we need to first extract tracks into individual media files
1964    if options.on_demand:
1965        (audio_sets, video_sets, subtitles_sets, mp4_files) = SelectTracks(options, media_sources)
1966        media_sources = [x for x in media_sources if x.format == "webvtt"] # Keep subtitles
1967        for track in sum(list(audio_sets.values()) + list(video_sets.values()), []):
1968            print('Extracting track', track.id, 'from', GetMappedFileName(track.parent.media_source.filename))
1969            track_file = tempfile.NamedTemporaryFile(dir=options.output_dir, delete=False)
1970            TempFiles.append(track_file.name)
1971            track_file.close() # necessary on Windows
1972            MapFileName(track_file.name, path.basename(track_file.name) + ' = Extracted[track '+str(track.id) + ' from '+GetMappedFileName(track.parent.media_source.filename)+']')
1973
1974            Mp4Fragment(options,
1975                        track.parent.media_source.filename,
1976                        track_file.name,
1977                        track = str(track.id),
1978                        index = True,
1979                        copy_udta = True,
1980                        quiet = True)
1981
1982            media_source = MediaSource(options, track_file.name)
1983            media_source.spec = track.parent.media_source.spec
1984            media_sources.append(media_source)
1985
1986    # compute the KID(s) and encryption key(s)
1987    if options.encryption_key:
1988        ResolveEncryptionKeys(options)
1989    PrepareSources(options, media_sources)
1990
1991    # encrypt the input files if needed
1992    EncryptSources(options, media_sources)
1993
1994    # parse the media sources and select the audio and video tracks
1995    (audio_sets, video_sets, subtitles_sets, mp4_files) = SelectTracks(options, media_sources)
1996    subtitles_files = SelectSubtitlesFiles(options, media_sources)
1997
1998    # store lists of all tracks by type
1999    audio_tracks     = sum(list(audio_sets.values()),     [])
2000    video_tracks     = sum(list(video_sets.values()),     [])
2001    subtitles_tracks = sum(list(subtitles_sets.values()), [])
2002
2003    # check that we have at least one audio and one video
2004    if not audio_tracks and not video_tracks and not subtitles_tracks:
2005        PrintErrorAndExit('ERROR: no track selected')
2006
2007    # assign key info to tracks
2008    for track in audio_tracks + video_tracks + subtitles_tracks:
2009        track.key_info = track.parent.media_source.key_infos.get(track.id, track.key_info)
2010
2011    if Options.verbose:
2012        print('Audio:',     audio_sets)
2013        print('Video:',     video_sets)
2014        print('Subtitles:', subtitles_sets)
2015
2016        for track in audio_tracks + video_tracks + subtitles_tracks:
2017            message = 'Key info for ' + str(track) + ': '
2018            if track.key_info.get('key'):
2019                message += '[KID={}, KEY={}]'.format(track.key_info['kid'], track.key_info['key'])
2020            else:
2021                if track.key_info.get('kid'):
2022                    message += '[PRE-ENCRYPTED KID={}]'.format(track.key_info['kid'])
2023                else:
2024                    message += '[NOT ENCRYPTED]'
2025            print(message)
2026
2027    # check that segments are consistent between tracks of the same adaptation set
2028    for tracks in list(video_sets.values()):
2029        prev_track = None
2030        for track in tracks:
2031            if prev_track:
2032                # compute the total duration, excluding the last segment, which may be somewhat mismatched
2033                track_duration_truncated = reduce(operator.add, track.segment_scaled_durations[:-1], 0)
2034                prev_track_duration_truncated = reduce(operator.add, prev_track.segment_scaled_durations[:-1], 0)
2035                if track_duration_truncated != prev_track_duration_truncated:
2036                    sys.stderr.write('WARNING: video duration mismatch between "'+str(track)+'" and "'+str(prev_track)+'"\n')
2037            prev_track = track
2038
2039    # check that the video segments match for all video tracks in the same adaptation set
2040    for tracks in list(video_sets.values()):
2041        if len(tracks) > 1:
2042            anchor = tracks[0]
2043            for track in tracks[1:]:
2044                if track.segment_scaled_durations[:-1] != anchor.segment_scaled_durations[:-1]:
2045                    PrintErrorAndExit('ERROR: video tracks are not aligned ("'+str(track)+'" differs from '+str(anchor)+')')
2046
2047    # check that the video segment durations are almost all equal
2048    if not options.use_segment_timeline:
2049        for video_track in video_tracks:
2050            for segment_duration in video_track.segment_durations[:-2]:
2051                ratio = segment_duration/video_track.average_segment_duration
2052                if ratio > 1.1 or ratio < 0.9:
2053                    sys.stderr.write('WARNING: video segment durations for "' + str(video_track) + '" vary by more than 10% (consider using --use-segment-timeline)\n')
2054                    break
2055        for audio_track in audio_tracks:
2056            for segment_duration in audio_track.segment_durations[:-2]:
2057                ratio = segment_duration/audio_track.average_segment_duration
2058                if ratio > 1.1 or ratio < 0.9:
2059                    sys.stderr.write('WARNING: audio segment durations for "' + str(audio_track) + '" vary by more than 10% (consider using --use-segment-timeline)\n')
2060                    break
2061
2062    # round the audio segment durations to be equal to the video segment durations
2063    if video_tracks:
2064        for audio_track in audio_tracks:
2065            ratio = audio_track.average_segment_duration/video_tracks[0].average_segment_duration
2066            if abs(ratio-1.0) < 0.05:
2067                # within 5%, make it equal
2068                if options.verbose:
2069                    print('INFO: adjusting segment duration for audio track '+str(audio_track)+' to '+str(video_tracks[0].average_segment_duration)+' to match the video')
2070                audio_track.average_segment_duration = video_tracks[0].average_segment_duration
2071
2072    # compute the representation id and init segment name for each track
2073    for adaptation_sets in [audio_sets, video_sets, subtitles_sets]:
2074        for adaptation_set_name, tracks in list(adaptation_sets.items()):
2075            for track in tracks:
2076                if not hasattr(track, 'representation_id'):
2077                    if options.split:
2078                        track.representation_id = '/'.join(adaptation_set_name)
2079                        if len(tracks) > 1:
2080                            track.representation_id += '/'+str(track.order_index)
2081                    else:
2082                        track.representation_id = '-'.join(adaptation_set_name)
2083                        if len(tracks) > 1:
2084                            track.representation_id += '-'+str(track.order_index)
2085
2086                if options.split:
2087                    track.init_segment_name = SPLIT_INIT_SEGMENT_NAME
2088                elif options.on_demand:
2089                    track.parent.media_name = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, track.representation_id)
2090                else:
2091                    track.init_segment_name = NOSPLIT_INIT_FILE_PATTERN % (track.representation_id)
2092
2093                track.stream_id = adaptation_set_name[0]
2094                if adaptation_set_name[0] == 'audio':
2095                    track.stream_id += '_'+track.language
2096
2097    # compute index and init offsets for the on-demand profile
2098    if options.on_demand:
2099        for track in audio_tracks+video_tracks+subtitles_tracks:
2100            atoms = WalkAtoms(track.parent.media_source.filename, 'moof')
2101            for atom in atoms:
2102                if atom.type == 'sidx' and not hasattr(track, 'sidx_atom'):
2103                    track.sidx_atom = atom
2104                if atom.type == 'moov' and not hasattr(track, 'moov_atom'):
2105                    track.moov_atom = atom
2106
2107    # compute some values if not set
2108    if options.min_buffer_time == 0.0:
2109        if video_tracks:
2110            options.min_buffer_time = video_tracks[0].average_segment_duration
2111        else:
2112            options.min_buffer_time = audio_tracks[0].average_segment_duration
2113
2114    # print info about the tracks
2115    if options.verbose:
2116        for track in audio_tracks+video_tracks+subtitles_tracks:
2117            print(('{} track: {} - language={}, max bitrate={:.0f}, avg bitrate={:.0f}, req bandwidth={:.0f}, codec={}').format(
2118                  track.type,
2119                  str(track),
2120                  track.language,
2121                  track.max_segment_bitrate,
2122                  track.average_segment_bitrate,
2123                  track.bandwidth,
2124                  track.codec))
2125
2126    # deal with the max playout strategy if set
2127    if options.max_playout_rate_strategy:
2128        max_playout_rate = options.max_playout_rate_strategy.split(':')[1]
2129        lowest_bandwidth_track = None
2130        lowest_bandwidth = -1
2131        for video_track in video_tracks:
2132            if lowest_bandwidth < 0 or video_track.bandwidth < lowest_bandwidth:
2133                lowest_bandwidth = video_track.bandwidth
2134                lowest_bandwidth_track = video_track
2135        if lowest_bandwidth_track:
2136            lowest_bandwidth_track.max_playout_rate = max_playout_rate
2137
2138    # create the directories and split/copy/process the media if needed
2139    if not options.no_media:
2140        if options.split:
2141            for adaptation_sets in [audio_sets, video_sets, subtitles_sets]:
2142                for adaptation_set_name, tracks in list(adaptation_sets.items()):
2143                    for track in tracks:
2144                        out_dir = path.join(options.output_dir, track.representation_id)
2145                        MakeNewDir(out_dir, recursive=True)
2146                        print('Splitting media file ('+adaptation_set_name[0]+')', GetMappedFileName(track.parent.media_source.filename))
2147                        Mp4Split(options,
2148                                 track.parent.media_source.filename,
2149                                 track_id               = str(track.id),
2150                                 pattern_parameters     = 'N',
2151                                 start_number           = '1',
2152                                 init_segment           = path.join(out_dir, track.init_segment_name),
2153                                 media_segment          = path.join(out_dir, SEGMENT_PATTERN))
2154
2155        else:
2156            for mp4_file in list(mp4_files.values()):
2157                print('Processing and Copying media file', GetMappedFileName(mp4_file.media_source.filename))
2158                media_filename = path.join(options.output_dir, mp4_file.media_name)
2159                if not options.force_output and path.exists(media_filename):
2160                    PrintErrorAndExit('ERROR: file ' + media_filename + ' already exists')
2161
2162                shutil.copyfile(mp4_file.media_source.filename, media_filename)
2163            if options.smooth or options.hippo:
2164                for track in audio_tracks+video_tracks+subtitles_tracks:
2165                    Mp4Split(options,
2166                             track.parent.media_source.filename,
2167                             track_id     = str(track.id),
2168                             init_only    = True,
2169                             init_segment = path.join(options.output_dir, track.init_segment_name))
2170
2171        if subtitles_files:
2172            MakeNewDir(path.join(options.output_dir, 'subtitles'))
2173            for subtitles_file in subtitles_files:
2174                print('Processing and Copying subtitles file', GetMappedFileName(subtitles_file.media_source.filename))
2175                out_dir = path.join(options.output_dir, 'subtitles', subtitles_file.language)
2176                MakeNewDir(out_dir)
2177                media_filename = path.join(out_dir, subtitles_file.media_name)
2178                shutil.copyfile(subtitles_file.media_source.filename, media_filename)
2179
2180    # output the DASH MPD
2181    OutputDash(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files)
2182
2183    # output the HLS playlists
2184    if options.hls:
2185        OutputHls(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files)
2186
2187    # output the Smooth Manifests
2188    if options.smooth:
2189        OutputSmooth(options, audio_tracks, video_tracks)
2190
2191    # output the Hippo Manifest
2192    if options.hippo:
2193        OutputHippo(options, audio_tracks, video_tracks)
2194
2195###########################
2196if sys.version_info < (3,7,0):
2197    sys.stderr.write("ERROR: This tool must be run with Python 3.7 or above\n")
2198    sys.stderr.write("You are running Python version: "+sys.version+"\n")
2199    exit(1)
2200
2201if __name__ == '__main__':
2202    try:
2203        main()
2204    except Exception as err:
2205        if Options and Options.debug:
2206            raise
2207        else:
2208            PrintErrorAndExit('ERROR: {}\n'.format(str(err)))
2209    finally:
2210        for f in TempFiles:
2211            os.unlink(f)
2212