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