%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /home/waritko/build/Bento4/Source/Python/utils/
Upload File :
Create Path :
Current File : //home/waritko/build/Bento4/Source/Python/utils/mp4-dash.py

#!/usr/bin/env python

__author__    = 'Gilles Boccon-Gibod (bok@bok.net)'
__copyright__ = 'Copyright 2011-2016 Axiomatic Systems, LLC.'

###
# NOTE: this script needs Bento4 command line binaries to run
# You must place the 'mp4info' 'mp4dump', 'mp4encrypt', 'mp4fragment', and 'mp4split' binaries
# in a directory named 'bin/<platform>' at the same level as where
# this script is.
# <platform> depends on the platform you're running on:
# Mac OSX   --> platform = macosx
# Linux x86 --> platform = linux-x86
# Windows   --> platform = win32

from optparse import OptionParser
import shutil
import xml.etree.ElementTree as xml
from xml.dom.minidom import parseString
import tempfile
import fractions
import re
import platform
import sys
import math
from mp4utils import *
from subtitles import *

# setup main options
VERSION = "1.8.0"
SDK_REVISION = '623'
SCRIPT_PATH = path.abspath(path.dirname(__file__))
sys.path += [SCRIPT_PATH]

VIDEO_MIMETYPE              = 'video/mp4'
AUDIO_MIMETYPE              = 'audio/mp4'
SUBTITLES_MIMETYPE          = 'application/mp4'
VIDEO_DIR                   = 'video'
AUDIO_DIR                   = 'audio'
MPD_NS_COMPAT               = 'urn:mpeg:DASH:schema:MPD:2011'
MPD_NS                      = 'urn:mpeg:dash:schema:mpd:2011'
SPLIT_INIT_SEGMENT_NAME     = 'init.mp4'
NOSPLIT_INIT_FILE_PATTERN   = 'init-%s.mp4'
ONDEMAND_MEDIA_FILE_PATTERN = '%s-%s.mp4'

PADDED_SEGMENT_PATTERN      = 'seg-%04llu.m4s'
PADDED_SEGMENT_URL_PATTERN  = 'seg-%04d.m4s'
PADDED_SEGMENT_URL_TEMPLATE = '$RepresentationID$/seg-$Number%04d$.m4s'
NOPAD_SEGMENT_PATTERN       = 'seg-%llu.m4s'
NOPAD_SEGMENT_URL_PATTERN   = 'seg-%d.m4s'
NOPAD_SEGMENT_URL_TEMPLATE  = '$RepresentationID$/seg-$Number$.m4s'
SEGMENT_PATTERN             = NOPAD_SEGMENT_PATTERN
SEGMENT_URL_PATTERN         = NOPAD_SEGMENT_URL_PATTERN
SEGMENT_URL_TEMPLATE        = NOPAD_SEGMENT_URL_TEMPLATE

MEDIA_FILE_PATTERN          = '%s-%02d.mp4'

MARLIN_SCHEME_ID_URI        = 'urn:uuid:5E629AF5-38DA-4063-8977-97FFBD9902D4'
MARLIN_MAS_NAMESPACE        = 'urn:marlin:mas:1-0:services:schemas:mpd'
MARLIN_PSSH_SYSTEM_ID       = '69f908af481646ea910ccd5dcccb0a3a'

PLAYREADY_PSSH_SYSTEM_ID    = '9a04f07998404286ab92e65be0885f95'
PLAYREADY_SCHEME_ID_URI     = 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95'
PLAYREADY_SCHEME_ID_URI_V10 = 'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95'
PLAYREADY_MSPR_NAMESPACE    = 'urn:microsoft:playready'

WIDEVINE_PSSH_SYSTEM_ID     = 'edef8ba979d64acea3c827dcd51d21ed'
WIDEVINE_SCHEME_ID_URI      = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'

PRIMETIME_PSSH_SYSTEM_ID    = 'f239e769efa348509c16a903c6932efb'
PRIMETIME_SCHEME_ID_URI     = 'urn:uuid:F239E769-EFA3-4850-9C16-A903C6932EFB'

MPEG_COMMON_ENCRYPTION_SCHEME_ID_URI = 'urn:mpeg:dash:mp4protection:2011'

EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID = '1077efecc0b24d02ace33c1e52e2fb4b'
EME_COMMON_ENCRYPTION_SCHEME_ID_URI  = 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b'

SMOOTH_DEFAULT_TIMESCALE    = 10000000

SMIL_NAMESPACE              = 'http://www.w3.org/2001/SMIL20/Language'

CENC_2013_NAMESPACE         = 'urn:mpeg:cenc:2013'

DASH_DEFAULT_ROLE_NAMESPACE = 'urn:mpeg:dash:role:2011'

DASH_MEDIA_SEGMENT_URL_PATTERN_SMOOTH = "/QualityLevels($Bandwidth$)/Fragments(%s=$Time$)"
DASH_MEDIA_SEGMENT_URL_PATTERN_HIPPO  = '%s/Bitrate($Bandwidth$)/Fragment($Time$)'

HIPPO_MEDIA_SEGMENT_REGEXP_DEFAULT = '%s/Bitrate\\\\(%d\\\\)/Fragment\\\\((\\\\d+)\\\\)'
HIPPO_MEDIA_SEGMENT_GROUPS_DEFAULT = '["time"]'
HIPPO_MEDIA_SEGMENT_REGEXP_SMOOTH  = 'QualityLevels\\\\(%d\\\\)/Fragments\\\\(%s=(\\\\d+)\\\\)'
HIPPO_MEDIA_SEGMENT_GROUPS_SMOOTH  = '["time"]'

MPEG_DASH_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI     = 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011'
DOLBY_DIGITAL_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI = 'tag:dolby.com,2014:dash:audio_channel_configuration:2011'

ISOFF_MAIN_PROFILE          = 'urn:mpeg:dash:profile:isoff-main:2011'
ISOFF_LIVE_PROFILE          = 'urn:mpeg:dash:profile:isoff-live:2011'
ISOFF_ON_DEMAND_PROFILE     = 'urn:mpeg:dash:profile:isoff-on-demand:2011'
HBBTV_15_ISOFF_LIVE_PROFILE = 'urn:hbbtv:dash:profile:isoff-live:2012'
ProfileAliases = {
  'main':      ISOFF_MAIN_PROFILE,
  'live':      ISOFF_LIVE_PROFILE,
  'on-demand': ISOFF_ON_DEMAND_PROFILE,
  'hbbtv-1.5': HBBTV_15_ISOFF_LIVE_PROFILE
}

TempFiles = []

MpegCencSchemeMap = {
    'cenc': 'MPEG-CENC',
    'cbc1': 'MPEG-CBC1',
    'cens': 'MPEG-CENS',
    'cbcs': 'MPEG-CBCS'
}

#############################################
def AddSegmentList(options, container, subdir, track, use_byte_range=False):
    if subdir:
        prefix = subdir+'/'
    else:
        prefix = ''
    segment_list = xml.SubElement(container,
                                  'SegmentList',
                                  timescale='1000',
                                  duration=str(int(round(track.average_segment_duration*1000))))
    if use_byte_range:
        byte_range = str(track.parent.init_segment.position)+'-'+str(track.parent.init_segment.position+track.parent.init_segment.size-1)
        xml.SubElement(segment_list,
                       'Initialization',
                       sourceURL=prefix + track.parent.media_name,
                       range=byte_range)
    else:
        xml.SubElement(segment_list,
                       'Initialization',
                       sourceURL=prefix + track.init_segment_name)
    i = 1
    for segment_index in track.moofs:
        segment = track.parent.segments[segment_index]
        segment_offset = segment[0].position
        segment_length = reduce(operator.add, [atom.size for atom in segment], 0)
        if use_byte_range:
            byte_range = str(segment_offset) + '-' + str(segment_offset + segment_length - 1)
            xml.SubElement(segment_list,
                           'SegmentURL',
                           media=prefix + track.parent.media_name,
                           mediaRange=byte_range)
        else:
            xml.SubElement(segment_list,
                           'SegmentURL',
                           media=prefix + (SEGMENT_URL_PATTERN % i))
        i += 1


#############################################
def AddSegmentTemplate(options, container, init_segment_url, media_url_template_prefix, track, stream_name):
    if options.use_segment_list:
        return

    if options.use_segment_timeline or track.type == 'subtitles':
        url_template = SEGMENT_URL_TEMPLATE
        use_template_numbers = True
        if options.smooth:
            url_base = path.basename(options.smooth_server_manifest_filename)
            url_template = url_base + DASH_MEDIA_SEGMENT_URL_PATTERN_SMOOTH % stream_name
            use_template_numbers = False
        elif options.hippo:
            url_template = DASH_MEDIA_SEGMENT_URL_PATTERN_HIPPO % stream_name
            use_template_numbers = False

        args = [container, 'SegmentTemplate']
        kwargs = {'timescale': str(track.timescale),
                  'initialization': init_segment_url,
                  'media': url_template,
                  'startNumber': '1'} # (keep the @startNumber, even if not needed, because some clients like Silverlight want it)

        segment_template = xml.SubElement(*args, **kwargs)
        segment_timeline = xml.SubElement(segment_template, 'SegmentTimeline')
        repeat_count = 0
        for i in range(len(track.segment_scaled_durations)):
            duration = track.segment_scaled_durations[i]
            if i + 1 < len(track.segment_scaled_durations) and duration == track.segment_scaled_durations[i + 1]:
                repeat_count += 1
            else:
                args = [segment_timeline, 'S']
                kwargs = {'d': str(duration)}
                if repeat_count:
                    kwargs['r'] = str(repeat_count)

                xml.SubElement(*args, **kwargs)
                repeat_count = 0
    else:
        xml.SubElement(container,
                       'SegmentTemplate',
                       timescale='1000',
                       duration=str(int(round(track.average_segment_duration*1000))),
                       initialization=init_segment_url,
                       media=SEGMENT_URL_TEMPLATE,
                       startNumber='1') # (keep the @startNumber, even if not needed, because some clients like Silverlight want it)

#############################################
def AddSegments(options, container, track):
    if options.use_segment_list:
        if options.split:
            subdir = track.representation_id
            use_byte_range = False
        else:
            subdir = None
            use_byte_range = True
        AddSegmentList(options, container, subdir, track, use_byte_range)

#############################################
def AddContentProtection(options, container, tracks):
    kids = []
    for track in tracks:
        kid = track.kid
        if kid is None and not options.no_media:
            PrintErrorAndExit('ERROR: no encryption info found in track '+str(track))
        if kid not in kids:
            kids.append(kid)

    # resolve the default KID and KEY
    key_info = None
    if len(tracks):
        key_info = tracks[0].key_info

    if key_info:
        default_kid = key_info['kid']
        default_key = key_info['key']
    else:
        default_kid = kids[0]
        default_key = None

    # EME Common Encryption
    if options.eme_signaling in ['pssh-v0', 'pssh-v1']:
        container.append(xml.Comment(' EME Common Encryption '))
        xml.register_namespace('cenc', CENC_2013_NAMESPACE)
        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=EME_COMMON_ENCRYPTION_SCHEME_ID_URI, value=options.encryption_cenc_scheme)
        if options.eme_signaling == 'pssh-v1':
            pssh_box = MakePsshBoxV1(EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID.decode('hex'), [default_kid], '')
        else:
            pssh_box = MakePsshBox(EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID.decode('hex'), '')
        pssh_b64 = pssh_box.encode('base64').replace('\n', '')
        pssh = xml.SubElement(cp, '{' + CENC_2013_NAMESPACE + '}pssh')
        pssh.text = pssh_b64

    # MPEG Common Encryption
    container.append(xml.Comment(' MPEG Common Encryption '))
    xml.register_namespace('cenc', CENC_2013_NAMESPACE)
    cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=MPEG_COMMON_ENCRYPTION_SCHEME_ID_URI, value=options.encryption_cenc_scheme)
    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()
    cp.set('{'+CENC_2013_NAMESPACE+'}default_KID', default_kid_with_dashes)

    # Marlin
    if options.marlin:
        container.append(xml.Comment(' Marlin '))
        xml.register_namespace('mas', MARLIN_MAS_NAMESPACE)
        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=MARLIN_SCHEME_ID_URI)
        cids = xml.SubElement(cp, '{' + MARLIN_MAS_NAMESPACE + '}MarlinContentIds')
        for kid in kids:
            cid = xml.SubElement(cids, '{' + MARLIN_MAS_NAMESPACE + '}MarlinContentId')
            cid.text = 'urn:marlin:kid:' + kid

    # PlayReady
    if options.playready:
        container.append(xml.Comment(' PlayReady '))
        xml.register_namespace('mspr', PLAYREADY_MSPR_NAMESPACE)
        if HBBTV_15_ISOFF_LIVE_PROFILE in options.profiles:
            playread_scheme_id_uri = PLAYREADY_SCHEME_ID_URI_V10
        else:
            playread_scheme_id_uri = PLAYREADY_SCHEME_ID_URI
        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=playread_scheme_id_uri)
        if options.playready_header:
            header_bin = ComputePlayReadyHeader(options.playready_header, default_kid, default_key)
            header_b64 = header_bin.encode('base64').replace('\n', '')
            pro = xml.SubElement(cp, '{' + PLAYREADY_MSPR_NAMESPACE + '}pro')
            pro.text = header_b64

    # Widevine
    if options.widevine:
        container.append(xml.Comment(' Widevine '))
        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=WIDEVINE_SCHEME_ID_URI)
        if options.widevine_header:
            pssh_payload = ComputeWidevineHeader(options.widevine_header, default_kid)
            pssh_box = MakePsshBox(WIDEVINE_PSSH_SYSTEM_ID.decode('hex'), pssh_payload)
            pssh_b64 = pssh_box.encode('base64').replace('\n', '')
            pssh = xml.SubElement(cp, '{' + CENC_2013_NAMESPACE + '}pssh')
            pssh.text = pssh_b64

    # Primetime
    if options.primetime:
        container.append(xml.Comment(' Primetime '))
        cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=PRIMETIME_SCHEME_ID_URI)
        if options.primetime_metadata:
            pssh_payload = ComputePrimetimeMetaData(options.primetime_metadata, default_kid)
            pssh_box = MakePsshBox(PRIMETIME_PSSH_SYSTEM_ID.decode('hex'), pssh_payload)
            pssh_b64 = pssh_box.encode('base64').replace('\n', '')
            pssh = xml.SubElement(cp, '{' + CENC_2013_NAMESPACE + '}pssh')
            pssh.text = pssh_b64

#############################################
def AddDescriptor(adaptation_set, set_attributes, set_name, category_name):
    attributes = set_attributes.get(set_name)
    if not attributes and category_name:
        # try a catch-all category set name
        attributes = set_attributes.get(category_name)
        if attributes:
            set_name = category_name
    if not attributes: return

    for descriptor_name in attributes:
        descriptor_values = attributes[descriptor_name]
        for descriptor_value in descriptor_values.split(','):
            descriptor_namespace = None

            if descriptor_name.lower() == 'role':
                descriptor_namespace = DASH_DEFAULT_ROLE_NAMESPACE
            elif descriptor_name.startswith('{'):
                closeparen = descriptor_name.find('}')
                if closeparen >= 0:
                    descriptor_namespace = descriptor_name[1:closeparen]
                    descriptor_name = descriptor_name[closeparen+1:]

            # support using lowercase names instead of capitalized names
            if descriptor_name == 'accessibility': descriptor_name = 'Accessibility'
            if descriptor_name == 'role':          descriptor_name = 'Role'
            if descriptor_name == 'rating':        descriptor_name = 'Rating'
            if descriptor_name == 'viewpoint':     descriptor_name = 'Viewpoint'

            if descriptor_name not in ['Accessibility', 'Role', 'Rating', 'Viewpoint']:
                continue
            if descriptor_namespace:
                xml.SubElement(adaptation_set,
                               descriptor_name,
                               schemeIdUri=descriptor_namespace,
                               value=descriptor_value)
            else:
                sys.stderr.write('WARNING: ignoring ' + descriptor_name + ' descriptor for set "' + set_name + '", the schemeIdUri must be specified\n')

#############################################
def OutputDash(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files):
    all_audio_tracks     = sum(audio_sets.values(),     [])
    all_video_tracks     = sum(video_sets.values(),     [])
    all_subtitles_tracks = sum(subtitles_sets.values(), [])

    # compute the total duration (we take the duration of the video)
    if len(all_video_tracks):
        presentation_duration = all_video_tracks[0].total_duration
    elif len(all_audio_tracks):
        presentation_duration = all_audio_tracks[0].total_duration
    elif len(all_subtitles_tracks):
        presentation_duration = all_subtitles_tracks[0].total_duration
    else:
        return

    # create the MPD
    if options.use_compat_namespace:
        mpd_ns = MPD_NS_COMPAT
    else:
        mpd_ns = MPD_NS
    mpd = xml.Element('MPD',
                      xmlns=mpd_ns,
                      profiles=','.join(options.profiles),
                      minBufferTime="PT%.02fS" % options.min_buffer_time,
                      mediaPresentationDuration=XmlDuration(presentation_duration),
                      type='static')
    mpd.append(xml.Comment(' Created with Bento4 mp4-dash.py, VERSION=' + VERSION + '-' + SDK_REVISION + ' '))
    period = xml.SubElement(mpd, 'Period')

    # process the video tracks
    if len(video_sets):
        period.append(xml.Comment(' Video '))

        for video_tracks in video_sets.values():
            # compute the min and max values
            minWidth  = 0
            minHeight = 0
            maxWidth  = 0
            maxHeight = 0
            for video_track in video_tracks:
                if minWidth  == 0 or video_track.width < minWidth:  minWidth  = video_track.width
                if minHeight == 0 or video_track.height < minHeight: minHeight = video_track.height
                if video_track.width  > maxWidth:  maxWidth  = video_track.width
                if video_track.height > maxHeight: maxHeight = video_track.height

            adaptation_set = xml.SubElement(period,
                                            'AdaptationSet',
                                            mimeType=VIDEO_MIMETYPE,
                                            segmentAlignment='true',
                                            startWithSAP='1',
                                            minWidth=str(minWidth),
                                            maxWidth=str(maxWidth),
                                            minHeight=str(minHeight),
                                            maxHeight=str(maxHeight))

            # see if we have descriptors
            AddDescriptor(adaptation_set, set_attributes, 'video', None)

            # setup content protection
            if options.encryption_key or options.marlin or options.playready or options.widevine:
                AddContentProtection(options, adaptation_set, video_tracks)

            if not options.on_demand:
                if options.split:
                    init_segment_url                  = '$RepresentationID$/' + SPLIT_INIT_SEGMENT_NAME
                    media_segment_url_template_prefix = '$RepresentationID$/'
                else:
                    init_segment_url                  = NOSPLIT_INIT_FILE_PATTERN % ('$RepresentationID$')
                    media_segment_url_template_prefix = ''
                AddSegmentTemplate(options, adaptation_set, init_segment_url, media_segment_url_template_prefix, video_tracks[0], 'video')

            for video_track in video_tracks:
                representation = xml.SubElement(adaptation_set,
                                                'Representation',
                                                id=video_track.representation_id,
                                                codecs=video_track.codec,
                                                width=str(video_track.width),
                                                height=str(video_track.height),
                                                scanType=video_track.scan_type,
                                                frameRate=video_track.frame_rate_ratio,
                                                bandwidth=str(video_track.bandwidth))
                if hasattr(video_track, 'max_playout_rate'):
                    representation.set('maxPlayoutRate', video_track.max_playout_rate)

                if options.on_demand:
                    base_url = xml.SubElement(representation, 'BaseURL')
                    base_url.text = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, video_track.representation_id)
                    sidx_range = (video_track.sidx_atom.position, video_track.sidx_atom.position+video_track.sidx_atom.size-1)
                    init_range = (0, video_track.moov_atom.position+video_track.moov_atom.size-1)
                    segment_base = xml.SubElement(representation, 'SegmentBase', indexRange=str(sidx_range[0])+'-'+str(sidx_range[1]))
                    xml.SubElement(segment_base, 'Initialization', range=str(init_range[0])+'-'+str(init_range[1]))
                else:
                    AddSegments(options, representation, video_track)

    # process the audio tracks
    if len(audio_sets):
        period.append(xml.Comment(' Audio '))
        for adaptation_set_name, audio_tracks in audio_sets.items():
            args = [period, 'AdaptationSet']
            kwargs = {'mimeType': AUDIO_MIMETYPE, 'startWithSAP': '1', 'segmentAlignment': 'true'}
            language = audio_tracks[0].language
            if (language != 'und') or options.always_output_lang:
                kwargs['lang'] = language
            adaptation_set = xml.SubElement(*args, **kwargs)

            # see if we have descriptors
            AddDescriptor(adaptation_set, set_attributes, 'audio/' + language, 'audio')

            # setup content protection
            if options.encryption_key or options.marlin or options.playready or options.widevine:
                AddContentProtection(options, adaptation_set, audio_tracks)

            if not options.on_demand:
                if options.split:
                    init_segment_url                  = '$RepresentationID$/' + SPLIT_INIT_SEGMENT_NAME
                    media_segment_url_template_prefix = '$RepresentationID$/'
                else:
                    init_segment_url                  = NOSPLIT_INIT_FILE_PATTERN % ('$RepresentationID$')
                    media_segment_url_template_prefix = ''

                stream_name = 'audio_' + language
                AddSegmentTemplate(options, adaptation_set, init_segment_url, media_segment_url_template_prefix, audio_tracks[0], stream_name)

            for audio_track in audio_tracks:
                representation = xml.SubElement(adaptation_set,
                                                'Representation',
                                                id=audio_track.representation_id,
                                                codecs=audio_track.codec,
                                                bandwidth=str(audio_track.bandwidth),
                                                audioSamplingRate=str(audio_track.sample_rate))
                if audio_track.codec == 'ec-3':
                    audio_channel_config_value = ComputeDolbyDigitalAudioChannelConfig(audio_track)
                    scheme_id_uri = DOLBY_DIGITAL_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI
                else:
                    audio_channel_config_value = str(audio_track.channels)
                    scheme_id_uri = MPEG_DASH_AUDIO_CHANNEL_CONFIGURATION_SCHEME_ID_URI
                audio_channel_config = xml.SubElement(representation,
                                                      'AudioChannelConfiguration',
                                                      schemeIdUri=scheme_id_uri,
                                                      value=audio_channel_config_value)

                if options.on_demand:
                    base_url = xml.SubElement(representation, 'BaseURL')
                    base_url.text = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, audio_track.representation_id)
                    sidx_range = (audio_track.sidx_atom.position, audio_track.sidx_atom.position+audio_track.sidx_atom.size-1)
                    init_range = (0, audio_track.moov_atom.position+audio_track.moov_atom.size-1)
                    segment_base = xml.SubElement(representation, 'SegmentBase', indexRange=str(sidx_range[0])+'-'+str(sidx_range[1]))
                    xml.SubElement(segment_base, 'Initialization', range=str(init_range[0])+'-'+str(init_range[1]))
                else:
                    AddSegments(options, representation, audio_track)

    # process all the subtitles tracks
    if len(subtitles_sets):
        period.append(xml.Comment(' Subtitles (Encapsulated) '))
        for adaptation_set_name, subtitles_tracks in subtitles_sets.items():
            for subtitles_track in subtitles_tracks:
                args = [period, 'AdaptationSet']
                kwargs = {'mimeType': SUBTITLES_MIMETYPE, 'startWithSAP': '1'}
                if (subtitles_track.language != 'und') or options.always_output_lang:
                    kwargs['lang'] = subtitles_track.language
                adaptation_set = xml.SubElement(*args, **kwargs)

                # add a 'subtitles' role
                xml.SubElement(adaptation_set, 'Role', schemeIdUri='urn:mpeg:dash:role:2011', value='subtitle')

                # see if we have other descriptors
                AddDescriptor(adaptation_set, set_attributes, 'subtitles/' + subtitles_track.language, 'subtitles')

                representation = xml.SubElement(adaptation_set,
                                                'Representation',
                                                id=subtitles_track.representation_id,
                                                codecs=subtitles_track.codec,
                                                bandwidth=str(subtitles_track.bandwidth))

                if options.on_demand:
                    base_url = xml.SubElement(representation, 'BaseURL')
                    base_url.text = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, subtitles_track.representation_id)
                    sidx_range = (subtitles_track.sidx_atom.position, subtitles_track.sidx_atom.position+subtitles_track.sidx_atom.size-1)
                    init_range = (0, subtitles_track.moov_atom.position+subtitles_track.moov_atom.size-1)
                    segment_base = xml.SubElement(representation, 'SegmentBase', indexRange=str(sidx_range[0])+'-'+str(sidx_range[1]))
                    xml.SubElement(segment_base, 'Initialization', range=str(init_range[0])+'-'+str(init_range[1]))
                else:
                    if options.split:
                        init_segment_url                  = '$RepresentationID$/' + SPLIT_INIT_SEGMENT_NAME
                        media_segment_url_template_prefix = '$RepresentationID$/'
                    else:
                        init_segment_url                  = NOSPLIT_INIT_FILE_PATTERN % ('$RepresentationID$')
                        media_segment_url_template_prefix = ''

                    stream_name = 'subtitles_' + subtitles_track.language
                    AddSegmentTemplate(options, adaptation_set, init_segment_url, media_segment_url_template_prefix, subtitles_track, stream_name)
                    AddSegments(options, representation, subtitles_track)

    # process all the subtitles files
    if len(subtitles_files):
        period.append(xml.Comment(' Subtitles (Sidecar) '))
        for subtitles_file in subtitles_files:
            args = [period, 'AdaptationSet']
            kwargs = {
                'mimeType':    subtitles_file.mime_type,
                'contentType': 'text'
            }
            if (subtitles_file.language != None):
                kwargs['lang'] = subtitles_file.language
            adaptation_set = xml.SubElement(*args, **kwargs)

            # add a 'subtitles' role
            xml.SubElement(adaptation_set, 'Role', schemeIdUri='urn:mpeg:dash:role:2011', value='subtitle')

            # see if we have other descriptors
            AddDescriptor(adaptation_set, set_attributes, 'subtitles/' + subtitles_file.language, 'subtitles')

            # estimate the bandwidth
            bandwidth = 1024 # default
            if presentation_duration and subtitles_file.size:
                bandwidth = int((8*subtitles_file.size)/presentation_duration)

            representation = xml.SubElement(adaptation_set,
                                            'Representation',
                                            id='subtitles/'+subtitles_file.language,
                                            bandwidth=str(bandwidth))
            base_url = xml.SubElement(representation, 'BaseURL')
            base_url.text = 'subtitles/'+subtitles_file.language+'/'+subtitles_file.media_name

    # save the MPD
    if options.mpd_filename:
        mpd_xml = parseString(xml.tostring(mpd)).toprettyxml("  ")
        # use a regex to fix a bug in toprettyxml() that inserts newlines in text content
        mpd_xml = re.sub(r'((?<=>)(\n[\s]*)(?=[^<\s]))|(?<=[^>\s])(\n[\s]*)(?=<)', '', mpd_xml)
        open(path.join(options.output_dir, options.mpd_filename), "wb").write(mpd_xml)


#############################################
def ComputeHlsWidevineKeyLine(options, track):
    try:
        pairs = options.widevine_header.split('#')
        fields = {}
        for pair in pairs:
            name, value = pair.split(':', 1)
            fields[name] = value
    except:
        raise Exception('invalid syntax for --widevine-header option')
    
    if 'content_id' not in fields:
        fields['content_id'] = '*'
    if 'kid' not in fields:
        fields['kid'] = track.key_info['kid']

    json_param = '{ "provider": "%(provider)s", "content_id": "%(content_id)s", "key_ids": ["%(kid)s"] }' % fields
    key_line   = 'URI="data:text/plain;base64,'+json_param.encode('base64').replace('\n','')+'",KEYFORMAT="com.widevine",KEYFORMATVERSIONS="1",IV=0x'+track.key_info['iv']

    return key_line

#############################################
def ComputeHlsFairplayKeyLine(options):
    return 'URI="'+options.fairplay_key_uri+'",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"'

#############################################
def OutputHlsCommon(options, track, media_subdir, playlist_name, media_file_name):
    hls_target_duration = math.ceil(max(track.segment_durations))

    playlist_file = open(path.join(options.output_dir, media_subdir, playlist_name), 'wb+')
    playlist_file.write('#EXTM3U\r\n')
    playlist_file.write('# Created with Bento4 mp4-dash.py, VERSION=' + VERSION + '-' + SDK_REVISION+'\r\n')
    playlist_file.write('#\r\n')
    playlist_file.write('#EXT-X-VERSION:6\r\n')
    playlist_file.write('#EXT-X-PLAYLIST-TYPE:VOD\r\n')
    playlist_file.write('#EXT-X-INDEPENDENT-SEGMENTS\r\n')
    playlist_file.write('#EXT-X-TARGETDURATION:%d\r\n' % (hls_target_duration))
    playlist_file.write('#EXT-X-MEDIA-SEQUENCE:0\r\n')
    if options.split:
        playlist_file.write('#EXT-X-MAP:URI="%s"\r\n' % (SPLIT_INIT_SEGMENT_NAME))
    else:
        init_segment_size = track.parent.init_segment.position + track.parent.init_segment.size
        playlist_file.write('#EXT-X-MAP:URI="%s",BYTERANGE="%d@0"\r\n' % (media_file_name, init_segment_size))

    if options.encryption_key:
        key_lines = []
        if options.fairplay_key_uri:
            key_lines.append(ComputeHlsFairplayKeyLine(options))
        if options.widevine_header:
            key_lines.append(ComputeHlsWidevineKeyLine(options, track))

        if len(key_lines) == 0:
            key_lines.append('URI="'+options.hls_key_url+'",IV=0x'+track.key_info['iv'])
        
        for key_line in key_lines:
            playlist_file.write('#EXT-X-KEY:METHOD=SAMPLE-AES,'+key_line+'\r\n')

    return playlist_file

#############################################
def OutputHlsTrack(options, track, media_subdir, media_playlist_name, media_file_name):
    media_playlist_file = OutputHlsCommon(options, track, media_subdir, media_playlist_name, media_file_name)

    if options.split:
        segment_pattern = SEGMENT_PATTERN.replace('ll','')

    for i in range(len(track.segment_durations)):
        media_playlist_file.write('#EXTINF:%f,\r\n' % (track.segment_durations[i]))
        if options.on_demand or not options.split:
            segment          = track.parent.segments[track.moofs[i]]
            segment_position = segment[0].position
            segment_size     = reduce(operator.add, [atom.size for atom in segment], 0)
            media_playlist_file.write('#EXT-X-BYTERANGE:%d@%d\r\n' % (segment_size, segment_position))
            media_playlist_file.write(media_file_name)
        else:
            media_playlist_file.write(segment_pattern % (i+1))
        media_playlist_file.write('\r\n')

    media_playlist_file.write('#EXT-X-ENDLIST\r\n')

#############################################
def OutputHlsIframeIndex(options, track, media_subdir, iframes_playlist_name, media_file_name):
    index_playlist_file = OutputHlsCommon(options, track, media_subdir, iframes_playlist_name, media_file_name)

    index_playlist_file.write('#EXT-X-I-FRAMES-ONLY\r\n')

    if not options.split:
        # get the I-frame index for a single file
        json_index = Mp4IframIndex(options, path.join(options.output_dir, media_file_name))
        index = json.loads(json_index)
        for i in range(len(track.segment_durations)):
            if i < len(index):
                index_entry = index[i]
                index_playlist_file.write('#EXTINF:%f,\r\n' % (track.segment_durations[i]))
                fragment_start    = int(index_entry['fragmentStart'])
                iframe_offset     = int(index_entry['offset'])
                iframe_size       = int(index_entry['size'])
                iframe_range_size = iframe_size + (iframe_offset-fragment_start)
                index_playlist_file.write('#EXT-X-BYTERANGE:%d@%d\r\n' % (iframe_range_size, fragment_start))
                index_playlist_file.write(media_file_name+'\r\n')
    else:
        segment_pattern = SEGMENT_PATTERN.replace('ll','')
        for i in range(len(track.segment_durations)):
            fragment_basename = segment_pattern % (i+1)
            fragment_file = path.join(options.output_dir, media_subdir, fragment_basename)
            init_file = path.join(options.output_dir, media_subdir, options.init_segment)
            if not path.exists(fragment_file):
                break
            json_index = Mp4IframIndex(options, fragment_file, fragments_info=init_file)
            index = json.loads(json_index)
            if len(index) < 1:
                break
            iframe_size       = int(index[0]['size'])
            iframe_offset     = int(index[0]['offset'])
            iframe_range_size = iframe_size + iframe_offset
            index_playlist_file.write('#EXTINF:%f,\r\n' % (track.segment_durations[i]))
            index_playlist_file.write('#EXT-X-BYTERANGE:%d@0\r\n' % (iframe_range_size))
            index_playlist_file.write(fragment_basename+'\r\n')
        
    index_playlist_file.write('#EXT-X-ENDLIST\r\n')

#############################################
def OutputHls(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files):
    all_audio_tracks     = sum(audio_sets.values(),     [])
    all_video_tracks     = sum(video_sets.values(),     [])
    all_subtitles_tracks = sum(subtitles_sets.values(), [])

    master_playlist_file = open(path.join(options.output_dir, options.hls_master_playlist_name), 'wb+')
    master_playlist_file.write('#EXTM3U\r\n')
    master_playlist_file.write('# Created with Bento4 mp4-dash.py, VERSION=' + VERSION + '-' + SDK_REVISION+'\r\n')
    master_playlist_file.write('#\r\n')
    master_playlist_file.write('#EXT-X-VERSION:6\r\n')
    master_playlist_file.write('\r\n')
    master_playlist_file.write('# Media Playlists\r\n')

    master_playlist_file.write('\r\n')
    master_playlist_file.write('# Audio\r\n')
    audio_groups = {}
    for adaptation_set_name, audio_tracks in audio_sets.items():
        language = audio_tracks[0].language.decode('utf-8')
        language_name = LanguageNames.get(language, language).decode('utf-8')

        audio_group_name = adaptation_set_name[0]+'/'+adaptation_set_name[2]
        audio_groups[audio_group_name] = {
            'codec': '',
            'average_segment_bitrate': 0,
            'max_segment_bitrate': 0
        }
        for audio_track in audio_tracks:
            # update the avergage and max bitrates
            if audio_track.average_segment_bitrate > audio_groups[audio_group_name]['average_segment_bitrate']:
                audio_groups[audio_group_name]['average_segment_bitrate'] = audio_track.average_segment_bitrate
            if audio_track.max_segment_bitrate > audio_groups[audio_group_name]['max_segment_bitrate']:
                audio_groups[audio_group_name]['max_segment_bitrate'] = audio_track.max_segment_bitrate

            # update/check the codec
            if audio_groups[audio_group_name]['codec'] == '':
                audio_groups[audio_group_name]['codec'] = audio_track.codec
            else:
                if audio_groups[audio_group_name]['codec'] != audio_track.codec:
                    print 'WARNING: audio codecs not all the same:', audio_groups[audio_group_name]['codec'], audio_track.codec

            if options.on_demand or not options.split:
                media_subdir        = ''
                media_file_name     = audio_track.parent.media_name
                media_playlist_name = audio_track.representation_id+".m3u8"
                media_playlist_path = media_playlist_name
            else:
                media_subdir        = audio_track.representation_id
                media_file_name     = ''
                media_playlist_name = options.hls_media_playlist_name
                media_playlist_path = media_subdir+'/'+media_playlist_name

            master_playlist_file.write(('#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="%s",LANGUAGE="%s",NAME="%s",AUTOSELECT=YES,DEFAULT=YES,URI="%s"\r\n' % (
                                        audio_group_name,
                                        language,
                                        language_name,
                                        media_playlist_path)).encode('utf-8'))
            OutputHlsTrack(options, audio_track, media_subdir, media_playlist_name, media_file_name)

    master_playlist_file.write('\r\n')
    master_playlist_file.write('# Video\r\n')
    iframe_playlist_lines = []
    for video_track in all_video_tracks:
        if options.on_demand or not options.split:
            media_subdir          = ''
            media_file_name       = video_track.parent.media_name
            media_playlist_name   = video_track.representation_id+".m3u8"
            media_playlist_path   = media_playlist_name
            iframes_playlist_name = video_track.representation_id+"_iframes.m3u8"
        else:
            media_subdir          = video_track.representation_id
            media_file_name       = ''
            media_playlist_name   = options.hls_media_playlist_name
            media_playlist_path   = media_subdir+'/'+media_playlist_name
            iframes_playlist_name = options.hls_iframes_playlist_name

        if len(audio_groups):
            # one entry per audio group
            for audio_group_name in audio_groups:
                audio_codec = audio_groups[audio_group_name]['codec']
                master_playlist_file.write('#EXT-X-STREAM-INF:AUDIO="%s",AVERAGE-BANDWIDTH=%d,BANDWIDTH=%d,CODECS="%s",RESOLUTION=%dx%d\r\n' % (
                                           audio_group_name,
                                           video_track.average_segment_bitrate + audio_groups[audio_group_name]['average_segment_bitrate'],
                                           video_track.max_segment_bitrate + audio_groups[audio_group_name]['max_segment_bitrate'],
                                           video_track.codec+','+audio_codec,
                                           video_track.width,
                                           video_track.height))
                master_playlist_file.write(media_playlist_path+'\r\n')
        else:
            # no audio
            master_playlist_file.write('#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=%d,BANDWIDTH=%d,CODECS="%s",RESOLUTION=%dx%d\r\n' % (
                                       video_track.average_segment_bitrate,
                                       video_track.max_segment_bitrate,
                                       video_track.codec,
                                       video_track.width,
                                       video_track.height))
            master_playlist_file.write(media_playlist_path+'\r\n')

        OutputHlsTrack(options, video_track, media_subdir, media_playlist_name, media_file_name)
        OutputHlsIframeIndex(options, video_track, media_subdir, iframes_playlist_name, media_file_name)

        # this will be written later
        iframe_playlist_lines.append('#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=%d,BANDWIDTH=%d,CODECS="%s",RESOLUTION=%dx%d,URI="%s"\r\n' % (
                                     video_track.average_segment_bitrate,
                                     video_track.max_segment_bitrate,
                                     video_track.codec,
                                     video_track.width,
                                     video_track.height,
                                     media_playlist_path))

    master_playlist_file.write('\r\n# I-Frame Playlists\r\n')
    master_playlist_file.write(''.join(iframe_playlist_lines))

    # IMSC1 subtitles
    if len(all_subtitles_tracks):
        master_playlist_file.write('\r\n# Subtitles (IMSC1)\r\n')
        for subtitles_track in all_subtitles_tracks:
            if subtitles_track.codec != 'stpp':
                # only accept IMSC1 tracks
                continue
            language = subtitles_track.language.decode('utf-8')
            language_name = LanguageNames.get(language, language).decode('utf-8')
            
            if options.on_demand or not options.split:
                media_subdir        = ''
                media_file_name     = subtitles_track.parent.media_name
                media_playlist_name = subtitles_track.representation_id+".m3u8"
                media_playlist_path = media_playlist_name
            else:
                media_subdir        = subtitles_track.representation_id
                media_file_name     = ''
                media_playlist_name = options.hls_media_playlist_name
                media_playlist_path = media_subdir+'/'+media_playlist_name

            master_playlist_file.write('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="imsc1",NAME="{0:s}",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="{1:s}",URI="{2:s}"\r\n'
                                       .format(language_name, language, media_playlist_path))
            
    # WebVTT subtitles
    if len(subtitles_files):
        master_playlist_file.write('\r\n# Subtitles (WebVTT)\r\n')
        for subtitles_file in subtitles_files:
            master_playlist_file.write('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="{0:s}",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="{0:s}",URI="subtitles/{0:s}/{1:s}"\r\n'
                                       .format(subtitles_file.language,subtitles_file.media_name))

#############################################
def OutputSmooth(options, audio_tracks, video_tracks):
    # compute the total duration (we take the duration of the video)
    if len(video_tracks):
        presentation_duration = video_tracks[0].total_duration
    else:
        presentation_duration = audio_tracks[0].total_duration

    # create the Client Manifest
    client_manifest = xml.Element('SmoothStreamingMedia',
                                  MajorVersion="2",
                                  MinorVersion="0",
                                  TimeScale="10000000",
                                  Duration=str(int(round(presentation_duration*10000000.0))))
    client_manifest.append(xml.Comment(' Created with Bento4 mp4-dash.py, VERSION='+VERSION+'-'+SDK_REVISION+' '))

    # process the audio tracks
    for audio_track in audio_tracks:
        stream_name = "audio_"+audio_track.language
        audio_url_pattern="QualityLevels({bitrate})/Fragments(%s={start time})" % (stream_name)
        stream_index = xml.SubElement(client_manifest,
                                      'StreamIndex',
                                      Chunks=str(len(audio_track.moofs)),
                                      Url=audio_url_pattern,
                                      Type="audio",
                                      Name=stream_name,
                                      QualityLevels="1",
                                      TimeScale=str(audio_track.timescale))
        if audio_track.language != 'und' or options.always_output_lang:
            stream_index.set('Language', audio_track.language)

        if audio_track.codec == 'ec-3':
            # Dolby Digital Plus
            (channels, codec_private_data) = ComputeDolbyDigitalSmoothStreamingInfo(audio_track)
            audio_tag = '65534'
            fourcc = 'EC-3'
            channels = str(channels)
            data_rate = int(audio_track.info['sample_descriptions'][0]['dolby_digital_info']['data_rate'])
            packet_size = str(4*data_rate)
        else:
            # assume AAC
            audio_tag = '255'
            fourcc = 'AACL'
            codec_private_data=audio_track.info['sample_descriptions'][0]['decoder_info']
            channels = str(audio_track.channels)
            packet_size = str(2*audio_track.channels)

        xml.SubElement(stream_index,
                       'QualityLevel',
                       Bitrate=str(audio_track.bandwidth),
                       SamplingRate=str(audio_track.sample_rate),
                       Channels=channels,
                       BitsPerSample="16",
                       PacketSize=packet_size,
                       AudioTag=audio_tag,
                       FourCC=fourcc,
                       Index="0",
                       CodecPrivateData=codec_private_data)

        for duration in audio_track.segment_scaled_durations:
            xml.SubElement(stream_index, "c", d=str(duration))

    # process all the video tracks
    if len(video_tracks):
        max_width  = max([track.width  for track in video_tracks])
        max_height = max([track.height for track in video_tracks])
        video_url_pattern="QualityLevels({bitrate})/Fragments(video={start time})"
        stream_index = xml.SubElement(client_manifest,
                                      'StreamIndex',
                                       Chunks=str(len(video_tracks[0].moofs)),
                                       Url=video_url_pattern,
                                       Type="video",
                                       Name="video",
                                       QualityLevels=str(len(video_tracks)),
                                       TimeScale=str(video_tracks[0].timescale),
                                       MaxWidth=str(max_width),
                                       MaxHeight=str(max_height))
        qindex = 0
        for video_track in video_tracks:
            sample_desc = video_track.info['sample_descriptions'][0]
            codec_private_data = '00000001'+sample_desc['avc_sps'][0]+'00000001'+sample_desc['avc_pps'][0]
            xml.SubElement(stream_index,
                           'QualityLevel',
                           Bitrate=str(video_track.bandwidth),
                           MaxWidth=str(video_track.width),
                           MaxHeight=str(video_track.height),
                           FourCC=options.smooth_h264_fourcc,
                           CodecPrivateData=codec_private_data,
                           Index=str(qindex))
            qindex += 1

        for duration in video_tracks[0].segment_scaled_durations:
            xml.SubElement(stream_index, "c", d=str(duration))

    if options.playready_header:
        key_info = None
        if options.encryption_key:
            if len(video_tracks):
                key_info = video_tracks[0].key_info
                if not key_info and len(audio_tracks):
                    key_info = audio_tracks[0].key_info

        if key_info:
            kid = key_info['kid']
            key = key_info['key']
        else:
            if len(video_tracks):
                kid = video_tracks[0].kid
            else:
                kid = audio_tracks[0].kid
            key = None
        header_bin = ComputePlayReadyHeader(options.playready_header, kid, key)
        header_b64 = header_bin.encode('base64').replace('\n', '')
        protection = xml.SubElement(client_manifest, 'Protection')
        protection_header = xml.SubElement(protection,
                                           'ProtectionHeader',
                                           SystemID='9a04f079-9840-4286-ab92-e65be0885f95')
        protection_header.text = header_b64

    # save the Smooth Client Manifest
    if options.smooth_client_manifest_filename != '':
        open(path.join(options.output_dir, options.smooth_client_manifest_filename), "wb").write(parseString(xml.tostring(client_manifest)).toprettyxml("  "))

    # create the Server Manifest file
    server_manifest = xml.Element('smil', xmlns=SMIL_NAMESPACE)
    server_manifest_head = xml.SubElement(server_manifest, 'head')
    xml.SubElement(server_manifest_head,
                   'meta',
                   name='clientManifestRelativePath',
                   content=path.basename(options.smooth_client_manifest_filename))
    server_manifest_body = xml.SubElement(server_manifest, 'body')
    server_manifest_switch = xml.SubElement(server_manifest_body, 'switch')
    for audio_track in audio_tracks:
        audio_entry = xml.SubElement(server_manifest_switch,
                                     'audio',
                                     src=audio_track.parent.media_name,
                                     systemBitrate=str(audio_track.bandwidth))
        xml.SubElement(audio_entry,
                       'param',
                       name='trackID',
                       value=str(audio_track.id),
                       valueType='data')
        if audio_track.language:
            xml.SubElement(audio_entry,
                           'param',
                           name='trackName',
                           value="audio_" + audio_track.language,
                           valueType='data')
        if audio_track.timescale != SMOOTH_DEFAULT_TIMESCALE:
            xml.SubElement(audio_entry,
                           'param',
                           name='timeScale',
                           value=str(audio_track.timescale),
                           valueType='data')

    for video_track in video_tracks:
        video_entry = xml.SubElement(server_manifest_switch,
                                     'video',
                                     src=video_track.parent.media_name,
                                     systemBitrate=str(video_track.bandwidth))
        xml.SubElement(video_entry,
                       'param',
                       name='trackID',
                       value=str(video_track.id),
                       valueType='data')
        if video_track.timescale != SMOOTH_DEFAULT_TIMESCALE:
            xml.SubElement(video_entry,
                           'param',
                           name='timeScale',
                           value=str(video_track.timescale),
                           valueType='data')

    # save the Manifest
    if options.smooth_server_manifest_filename != '':
        open(path.join(options.output_dir, options.smooth_server_manifest_filename), "wb").write(parseString(xml.tostring(server_manifest)).toprettyxml("  "))

#############################################
def OutputHippo(options, audio_tracks, video_tracks):
    # create the Server Manifest file
    server_manifest = '{\n  "media": [\n'
    sep = ''
    for track in audio_tracks+video_tracks:
        server_manifest += sep+'    {\n'
        server_manifest += '      "trackId": '+ str(track.id) + ',\n'
        server_manifest += '      "mediaSegments": {\n'
        server_manifest += '        "urls": [\n'
        server_manifest += '          {\n'
        if options.smooth:
            server_manifest += '            "pattern": "'+(HIPPO_MEDIA_SEGMENT_REGEXP_SMOOTH % (track.bandwidth, track.stream_id))+'",\n'
            server_manifest += '            "fields": '+HIPPO_MEDIA_SEGMENT_GROUPS_SMOOTH+'\n'
        else:
            server_manifest += '            "pattern": "'+(HIPPO_MEDIA_SEGMENT_REGEXP_DEFAULT % (track.stream_id, track.bandwidth))+'",\n'
            server_manifest += '            "fields": '+HIPPO_MEDIA_SEGMENT_GROUPS_DEFAULT+'\n'
        server_manifest += '          }\n'
        server_manifest += '        ],\n'
        server_manifest += '        "file": "' + track.parent.media_name + '"\n'
        server_manifest += '      },\n'
        server_manifest += '      "initSegment": {\n'
        server_manifest += '        "file": "' + track.init_segment_name + '"\n'
        server_manifest += '      }\n'
        server_manifest += '    }'
        sep = ',\n'
    server_manifest += '\n  ]\n}'

    # save the Manifest
    if options.hippo_server_manifest_filename != '':
        open(path.join(options.output_dir, options.hippo_server_manifest_filename), "wb").write(server_manifest)

#############################################
def SelectTracks(options, media_sources):
    # parse the media files
    file_list_index = 1
    mp4_files = {}
    mp4_media_names = []
    for media_source in media_sources:
        if media_source.format != 'mp4': continue

        media_file = media_source.filename

        # check if we have already parsed this file
        if media_file in mp4_files:
            media_source.mp4_file = mp4_files[media_file]
            continue

        # parse the file
        if not os.path.exists(media_file):
            PrintErrorAndExit('ERROR: media file ' + media_file + ' does not exist')

        # get the file info
        print 'Parsing media file', str(file_list_index)+':', GetMappedFileName(media_file)
        mp4_file = Mp4File(Options, media_source)

        # set some metadata properties for this file
        mp4_file.file_list_index = file_list_index
        file_list_index += 1
        if options.rename_media:
            mp4_file.media_name = MEDIA_FILE_PATTERN % (options.media_prefix, mp4_file.file_list_index)
        elif '+media' in media_source.spec:
            mp4_file.media_name = media_source.spec['+media']
        elif 'media' in media_source.spec: # we still accept the form without the '+' sign, for legacy support
            mp4_file.media_name = media_source.spec['media']
        else:
            mp4_file.media_name = path.basename(media_source.original_filename)

        if not options.split:
            if mp4_file.media_name in mp4_media_names:
                PrintErrorAndExit('ERROR: output media name %s is not unique, consider using --rename-media'%mp4_file.media_name)

        # check the file
        if mp4_file.info['movie']['fragments'] != True:
            PrintErrorAndExit('ERROR: file '+str(mp4_file.file_list_index)+' is not fragmented (use mp4fragment to fragment it)')

        # set the source property
        media_source.mp4_file = mp4_file
        mp4_files[media_file] = mp4_file
        mp4_media_names.append(mp4_file.media_name)


    # select tracks
    audio_adaptation_sets     = {}
    video_adaptation_sets     = {}
    subtitles_adaptation_sets = {}
    for media_source in media_sources:
        track_id       = media_source.spec['track']
        track_type     = media_source.spec['type']
        track_language = media_source.spec['language']
        tracks         = []

        if media_source.format != 'mp4':
            if track_id or track_type:
                PrintErrorAndExit('ERROR: track ID and track type selections only apply to MP4 media sources')

            continue

        if track_type not in ['', 'audio', 'video', 'subtitles']:
            sys.stderr.write('WARNING: ignoring source '+media_source.name+', unknown type')

        if track_id and track_type:
            PrintErrorAndExit('ERROR: track ID and track type selections are mutually exclusive')

        if track_id:
            tracks = [media_source.mp4_file.find_track_by_id(track_id)]
            if not tracks:
                PrintErrorAndExit('ERROR: track id not found for media file '+media_source.name)

        if track_type:
            tracks = media_source.mp4_file.find_tracks_by_type(track_type)
            if not tracks:
                PrintErrorAndExit('ERROR: no ' + track_type + ' found for media file '+media_source.name)

        if not tracks:
            tracks = media_source.mp4_file.tracks.values()

        # pre-process the track metadata
        for track in tracks:
            # track language
            language = LanguageCodeMap.get(track.language, track.language)
            if track_language and track_language != language and track_language != track.language:
                continue
            remap_language = media_source.spec.get('+language')
            if remap_language:
                language = remap_language
            elif options.language_map and language in options.language_map:
                language = options.language_map[language]
            track.language = language

            # video scan type
            if track.type == 'video':
                track.scan_type = media_source.spec.get('+scan_type', track.scan_type)

        # process audio tracks
        for track in [t for t in tracks if t.type == 'audio']:
            adaptation_set_name = ('audio', track.language, track.codec_family)
            adaptation_set = audio_adaptation_sets.get(adaptation_set_name, [])
            audio_adaptation_sets[adaptation_set_name] = adaptation_set

            # only keep this track if there isn't already a track with the same codec at the same bitrate (within 10%)
            with_same_bandwidth = [t for t in adaptation_set if abs(float(t.bandwidth-track.bandwidth)/float(t.bandwidth)) < 0.1]
            if len(with_same_bandwidth):
                continue

            adaptation_set.append(track)
            track.order_index = len(adaptation_set)

        # process video tracks
        for track in [t for t in tracks if t.type == 'video']:
            adaptation_set_name = ('video', track.codec_family)
            adaptation_set = video_adaptation_sets.get(adaptation_set_name, [])
            video_adaptation_sets[adaptation_set_name] = adaptation_set
            adaptation_set.append(track)
            track.order_index = len(adaptation_set)

        # process subtitle tracks
        if options.subtitles:
            for track in [t for t in tracks if t.type == 'subtitles']:
                adaptation_set_name = ('subtitles', track.language, track.codec_family)
                adaptation_set = subtitles_adaptation_sets.get(adaptation_set_name, [])
                subtitles_adaptation_sets[adaptation_set_name] = adaptation_set

                if len([et for et in adaptation_set if et.language == track.language]):
                    continue # only accept one track for each language

                adaptation_set.append(track)
                track.order_index = len(adaptation_set)

    return (audio_adaptation_sets, video_adaptation_sets, subtitles_adaptation_sets, mp4_files)

#############################################
def SelectSubtitlesFiles(options, media_sources):
    return [SubtitlesFile(options, media_source) for media_source in media_sources if media_source.format in ['ttml', 'webvtt']]

#############################################
def ResolveEncryptionKeys(options):
    options.key_infos = []
    for key_spec in options.encryption_key.split(','):
        key_info = {'filter': ['audio', 'video']}
        if key_spec.startswith('audio:') or key_spec.startswith('video:'):
            separator = key_spec.find(':')
            key_info['filter'] = [key_spec[:separator]]
            key_spec = key_spec[separator+1:]
        iv_hex = None
        if key_spec.startswith('@'):
            kid_hex, key_hex = GetEncryptionKey(options, key_spec[1:])
        else:
            if ':' not in key_spec:
                raise Exception('Invalid argument syntax for --encryption-key option')
            parts = key_spec.split(':', 2)
            kid_hex = parts[0]
            key_hex = parts[1]
            if len(parts) > 2:
                iv_hex = parts[2]
            if len(kid_hex) != 32:
                raise Exception('Invalid argument format for --encryption-key option')

            if key_hex.startswith('#'):
                if len(key_hex) != 41:
                    raise Exception('Invalid argument format for --encryption-key option')
                key_seed_bin = key_hex[1:].decode('base64')
                kid_bin = kid_hex.decode('hex')
                key_hex = DerivePlayReadyKey(key_seed_bin, kid_bin).encode('hex')
                if options.verbose:
                    print 'PlayReady Derived Key =', key_hex
            else:
                if len(key_hex) != 32:
                    raise Exception('Invalid argument format for --encryption-key option')

        key_info['key'] = key_hex
        key_info['kid'] = kid_hex
        key_info['iv']  = iv_hex or 'random'
        if options.hls and not iv_hex:
            # for HLS, we need to know the IV
            import random
            sys_random = random.SystemRandom()
            random_iv = sys_random.getrandbits(128)
            key_info['iv'] = '%016x' % random_iv

        options.key_infos.append(key_info)

#############################################
FileNameMap = {}
def MapFileName(from_name, to_name):
    global FileNameMap
    FileNameMap[from_name] = to_name

def GetMappedFileName(filename):
    return FileNameMap.get(filename, filename)

#############################################
Options = None
def main():
    # determine the platform binary name
    host_platform = ''
    if platform.system() == 'Linux':
        if platform.processor() == 'x86_64':
            host_platform = 'linux-x86_64'
        else:
            host_platform = 'linux-x86'
    elif platform.system() == 'Darwin':
        host_platform = 'macosx'
    elif platform.system() == 'Windows':
        host_platform = 'win32'
    default_exec_dir = path.join(SCRIPT_PATH, 'bin', host_platform)
    if not path.exists(default_exec_dir):
        default_exec_dir = path.join(SCRIPT_PATH, 'bin')
    if not path.exists(default_exec_dir):
        default_exec_dir = path.join(SCRIPT_PATH, '..', 'bin')

    # parse options
    parser = OptionParser(usage="%prog [options] <media-file> [<media-file> ...]",
                          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)
    parser.add_option('-v', '--verbose', dest="verbose", action='store_true', default=False,
                      help="Be verbose")
    parser.add_option('-d', '--debug', dest="debug", action='store_true', default=False,
                      help="Print out debugging information")
    parser.add_option('-o', '--output-dir', dest="output_dir",
                      help="Output directory", metavar="<output-dir>", default='output')
    parser.add_option('-f', '--force', dest="force_output", action="store_true",
                      help="Allow output to an existing directory", default=False)
    parser.add_option('', '--mpd-name', dest="mpd_filename",
                      help="MPD file name", metavar="<filename>", default='stream.mpd')
    parser.add_option('', '--profiles', dest='profiles',
                      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',
                      metavar="<profiles>")
    parser.add_option('', '--no-media', dest="no_media", action='store_true', default=False,
                      help="Do not output media files (MPD/Manifests only)")
    parser.add_option('', '--rename-media', dest='rename_media', action='store_true', default=False,
                      help = 'Use a file name pattern instead of the base name of input files for output media files.')
    parser.add_option('', '--media-prefix', dest='media_prefix', metavar='<prefix>', default='media',
                      help='Use this prefix for prefixed media file names (instead of the default prefix "media")')
    parser.add_option('', '--init-segment', dest="init_segment",
                      help="Initialization segment name", metavar="<filename>", default='init.mp4')
    parser.add_option('', "--no-split", action="store_false", dest="split", default=True,
                      help="Do not split the file into individual segment files")
    parser.add_option('', "--use-segment-list", action="store_true", dest="use_segment_list", default=False,
                      help="Use segment lists instead of segment templates")
    parser.add_option('', '--use-segment-template-number-padding', action='store_true', dest='segment_template_padding', default=False,
                      help="Use padded numbers in segment URL/filename templates")
    parser.add_option('', "--use-segment-timeline", action="store_true", dest="use_segment_timeline", default=False,
                      help="Use segment timelines (necessary if segment durations vary)")
    parser.add_option('', "--min-buffer-time", metavar='<duration>', dest="min_buffer_time", type="float", default=0.0,
                      help="Minimum buffer time (in seconds)")
    parser.add_option('', "--max-playout-rate", metavar='<strategy>', dest='max_playout_rate_strategy',
                      help="Max Playout Rate setting strategy for trick-play support. Supported strategies: lowest:X"),
    parser.add_option('', "--language-map", dest="language_map", metavar="<lang_from>:<lang_to>[,...]",
                      help="Remap language code <lang_from> to <lang_to>. Multiple mappings can be specified, separated by ','")
    parser.add_option('', "--always-output-lang", dest="always_output_lang", action='store_true', default=False,
                      help="Always output an @lang attribute for audio tracks even when the language is undefined"),
    parser.add_option('', "--subtitles", dest="subtitles", action="store_true", default=False,
                      help="Enable Subtitles")
    parser.add_option('', "--attributes", dest="attributes", action="append", metavar='<attributes-definition>', default=[],
                      help="Specify the attributes of a set of tracks. This option may be used multiple times, once per attribute set.")
    parser.add_option('', "--smooth", dest="smooth", default=False, action="store_true",
                      help="Produce an output compatible with Smooth Streaming")
    parser.add_option('', '--smooth-client-manifest-name', dest="smooth_client_manifest_filename",
                      help="Smooth Streaming Client Manifest file name", metavar="<filename>", default='stream.ismc')
    parser.add_option('', '--smooth-server-manifest-name', dest="smooth_server_manifest_filename",
                      help="Smooth Streaming Server Manifest file name", metavar="<filename>", default='stream.ism')
    parser.add_option('', '--smooth-h264-fourcc', dest='smooth_h264_fourcc',
                      help="Smooth Streaming FourCC value for H.264 video (default=H264) [some older players use AVC1]", metavar="<fourcc>", default='H264')
    parser.add_option('', '--hls', dest="hls", default=False, action="store_true",
                      help="Output HLS playlists in addition to MPEG DASH")
    parser.add_option('', '--hls-key-url', dest="hls_key_url",
                      help="HLS key URL (default: key.bin)", metavar="<url>", default='key.bin')
    parser.add_option('', '--hls-master-playlist-name', dest="hls_master_playlist_name",
                      help="HLS master playlist name (default: master.m3u8)", metavar="<filename>", default='master.m3u8')
    parser.add_option('', '--hls-media-playlist-name', dest="hls_media_playlist_name",
                      help="HLS media playlist name (default: media.m3u8)", metavar="<filename>", default='media.m3u8')
    parser.add_option('', '--hls-iframes-playlist-name', dest="hls_iframes_playlist_name",
                      help="HLS I-Frames playlist name (default: iframes.m3u8)", metavar="<filename>", default='iframes.m3u8')
    parser.add_option('', "--hippo", dest="hippo", default=False, action="store_true",
                      help="Produce an output compatible with the Hippo Media Server")
    parser.add_option('', '--hippo-server-manifest-name', dest="hippo_server_manifest_filename",
                      help="Hippo Media Server Manifest file name", metavar="<filename>", default='stream.msm')
    parser.add_option('', "--use-compat-namespace", dest="use_compat_namespace", action="store_true", default=False,
                      help="Use the original DASH MPD namespace as it was specified in the first published specification")
    parser.add_option('', "--encryption-key", dest="encryption_key", metavar='<key-spec>', default=None,
                      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: " +
                           "(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 " +
                           "(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)")
    parser.add_option('', "--encryption-cenc-scheme", dest="encryption_cenc_scheme", metavar='<cenc-scheme>', default='cenc', choices=('cenc', 'cbc1', 'cens', 'cbcs'),
                      help="MPEG Common Encryption scheme (cenc, cbc1, cens or cbcs). (default: cenc)")
    parser.add_option('', "--encryption-args", dest="encryption_args", metavar='<cmdline-arguments>', default=None,
                      help="Pass additional command line arguments to mp4encrypt (separated by spaces)")
    parser.add_option('', "--eme-signaling", dest="eme_signaling", metavar='<eme-signaling-type>', choices=['pssh-v0', 'pssh-v1'],
                      help="Add EME-compliant signaling in the MPD and PSSH boxes (valid options are 'pssh-v0' and 'pssh-v1')")
    parser.add_option('', "--marlin", dest="marlin", action="store_true", default=False,
                      help="Add Marlin signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
    parser.add_option('', "--marlin-add-pssh", dest="marlin_add_pssh", action="store_true", default=False,
                      help="Add an (optional) Marlin 'pssh' box in the init segment(s)")
    parser.add_option('', "--playready", dest="playready", action="store_true", default=False,
                      help="Add PlayReady signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
    parser.add_option('', "--playready-header", dest="playready_header", metavar='<playready-header>', default=None,
                      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. " +
                           "The <playready-header> argument can be either: " +
                           "(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 "
                           "(2) the character '#' followed by a PlayReady Header Object encoded in Base64, or " +
                           "(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)")
    parser.add_option('', "--playready-add-pssh", dest="playready_add_pssh", action="store_true", default=False,
                      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]")
    parser.add_option('', "--playready-no-pssh", dest="playready_no_pssh", action="store_true", default=False,
                      help="Do not store the PlayReady header in a 'pssh' box in the init segment(s)")
    parser.add_option('', "--widevine", dest="widevine", action="store_true", default=False,
                      help="Add Widevine signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
    parser.add_option('', "--widevine-header", dest="widevine_header", metavar='<widevine-header>', default=None,
                      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. " +
                           "The <widevine-header> argument can be either: " +
                           "(1) the character '#' followed by a Widevine header encoded in Base64, or " +
                           "(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])")
    parser.add_option('', "--primetime", dest="primetime", action="store_true", default=False,
                      help="Add Primetime signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
    parser.add_option('', "--primetime-metadata", dest="primetime_metadata", metavar='<primetime-metadata>', default=None,
                      help="Add Primetime metadata in a PSSH box in the init segments. The use of this option implies the --primetime option. " +
                           "The <primetime-data> argument can be either: " +
                           "(1) the character '@' followed by the name of a file containing the Primetime Metadata to use, or "
                           "(2) the character '#' followed by the Primetime Metadata encoded in Base64")
    parser.add_option('', "--fairplay-key-uri", dest="fairplay_key_uri",
                      help="Specify the key URI to use for FairPlay Streaming key delivery (only valid with --hls option)")
    parser.add_option('', "--exec-dir", metavar="<exec_dir>", dest="exec_dir", default=default_exec_dir,
                      help="Directory where the Bento4 executables are located")
    (options, args) = parser.parse_args()
    if len(args) == 0:
        parser.print_help()
        sys.exit(1)
    global Options
    Options = options

    # set some synthetic (not from command line) options
    options.on_demand = False

    # check the consistency of the options
    if options.smooth:
        options.split = False
        options.use_segment_timeline = True
        if options.use_segment_list:
            raise Exception('ERROR: --smooth and --use-segment-list are mutually exclusive')

    if options.hippo:
        options.split = False
        options.use_segment_timeline = True
        if options.use_segment_list:
            raise Exception('ERROR: --hippo and --use-segment-list are mutually exclusive')

    if not path.exists(options.exec_dir):
        PrintErrorAndExit('Executable directory does not exist ('+Options.exec_dir+'), use --exec-dir')

    if options.max_playout_rate_strategy:
        if not options.max_playout_rate_strategy.startswith('lowest:'):
            PrintErrorAndExit('Max Playout Rate strategy '+options.max_playout_rate_strategy+' is not supported')

    # switch variables
    if options.segment_template_padding:
        global SEGMENT_PATTERN, SEGMENT_URL_PATTERN, SEGMENT_URL_TEMPLATE
        SEGMENT_PATTERN      = PADDED_SEGMENT_PATTERN
        SEGMENT_URL_PATTERN  = PADDED_SEGMENT_URL_PATTERN
        SEGMENT_URL_TEMPLATE = PADDED_SEGMENT_URL_TEMPLATE

    # post-process some of the options
    if not options.profiles:
        if options.use_segment_list:
            options.profiles = [ISOFF_MAIN_PROFILE]
        else:
            options.profiles = [ISOFF_LIVE_PROFILE]
    else:
        profiles = []
        for profile in options.profiles.split(','):
            profile = profile.strip()
            if profile in ProfileAliases:
                profile = ProfileAliases[profile]
            profiles.append(profile)
        options.profiles = profiles
    if ISOFF_ON_DEMAND_PROFILE in options.profiles:
        options.on_demand = True
        options.split = False
        if ISOFF_LIVE_PROFILE in options.profiles:
            raise Exception('on-demand and live profiles are mutually exclusive')
    if HBBTV_15_ISOFF_LIVE_PROFILE in options.profiles:
        options.playready_no_pssh = True
        if options.playready_add_pssh:
            sys.stderr.write('INFO: since hbbtv-1.5 profile is selected, no PlayReady PSSH box will be added to the init segments\n')
        options.always_output_lang = True

    if not options.split:
        if not options.smooth and not options.hippo and not options.on_demand and not options.use_segment_list:
            sys.stderr.write('WARNING: --no-split requires --use-segment-list, which will be enabled automatically\n')
            options.use_segment_list = True

    if options.on_demand and options.use_segment_list:
        raise Exception('segment lists cannot be used with the on-demand profile')

    if options.smooth:
        if ISOFF_LIVE_PROFILE not in options.profiles:
            raise Exception('--smooth requires the live profile')
    if options.hippo:
        if ISOFF_LIVE_PROFILE not in options.profiles:
            raise Exception('--hippo requires the live profile')

    if options.verbose:
        print 'Profiles:', ','.join(options.profiles)

    if options.playready_header or options.playready_add_pssh:
        options.playready = True

    if options.playready:
        options.playready_add_pssh = True

    if options.playready_no_pssh:
        options.playready_add_pssh = False

    if options.widevine_header:
        options.widevine = True
        if options.hls and options.widevine_header.startswith('#'):
            raise Exception('with --hls, only the <name>:<value> pair syntax is supported for the --widevine-header option')

    if options.primetime_metadata:
        options.primetime = True

    if options.fairplay_key_uri:
        if not options.hls:
            sys.stderr.write('WARNING: --fairplay-key-uri is only valid with --hls, ignoring\n')
            
    if options.hls:
        if options.encryption_key and options.encryption_cenc_scheme != 'cbcs':
            raise Exception('--hls requires --encryption-cenc-scheme=cbcs')

    # compute the KID(s) and encryption key(s) if needed
    if options.encryption_key:
        ResolveEncryptionKeys(options)
        if options.verbose:
            for key_info in options.key_infos:
                if key_info['key']:
                    message = 'KID='+key_info['kid']+', KEY='+key_info['key']+', IV='+key_info['iv']
                else:
                    message = '[NOT ENCRYPTED]'
                print 'Key Info for '+'/'.join(key_info['filter'])+': '+message

    # process language map options
    if options.language_map:
        mappings = options.language_map.split(',')
        options.language_map = {}
        for mapping in mappings:
            from_lang, to_lang = mapping.split(':')
            options.language_map[from_lang] = to_lang

    # parse the attributes definitions
    set_attributes = {}
    for set_attributes_spec in options.attributes:
        try:
            set_name, attributes = set_attributes_spec.split(':', 1)
            set_attributes[set_name] = {}
            for attribute in attributes.split(','):
                name, value = attribute.split('=', 1)
                set_attributes[set_name][name] = value
        except:
            raise Exception('Invalid syntax for --attributes option')

    # parse media sources syntax
    media_sources = [MediaSource(source) for source in args]

    # create the output directory
    severity = 'ERROR'
    if options.no_media: severity = 'WARNING'
    if options.force_output: severity = None
    MakeNewDir(dir=options.output_dir, exit_if_exists = not (options.no_media or options.force_output), severity=severity)

    # for on-demand, we need to first extract tracks into individual media files
    if options.on_demand:
        (audio_sets, video_sets, subtitles_sets, mp4_files) = SelectTracks(options, media_sources)
        media_sources = filter(lambda x: x.format == "webvtt", media_sources) #Keep subtitles
        for track in sum(audio_sets.values()+video_sets.values(), []):
            print 'Extracting track', track.id, 'from', GetMappedFileName(track.parent.media_source.filename)
            track_file = tempfile.NamedTemporaryFile(dir = options.output_dir, delete=False)
            TempFiles.append(track_file.name)
            track_file.close() # necessary on Windows
            MapFileName(track_file.name, path.basename(track_file.name) + ' = Extracted[track '+str(track.id) + ' from '+GetMappedFileName(track.parent.media_source.filename)+']')

            Mp4Fragment(options,
                        track.parent.media_source.filename,
                        track_file.name,
                        track = str(track.id),
                        index = True,
                        quiet = True)

            media_source = MediaSource(track_file.name)
            media_source.spec = track.parent.media_source.spec
            media_sources.append(media_source)

    # encrypt the input files if needed
    if options.encryption_key:
        encrypted_files = {}
        for media_source in media_sources:
            if media_source.format != 'mp4': continue

            media_file = media_source.filename

            # check if we have already encrypted this file
            if media_file in encrypted_files:
                media_source.filename = encrypted_files[media_file].name
                continue

            # get the mp4 file info
            json_info = Mp4Info(Options, media_file, format='json', fast=True)
            info = json.loads(json_info, strict=False)

            if not info['movie']['fragments']:
                PrintErrorAndExit('ERROR: file '+media_file+' is not fragmented (use mp4fragment to fragment it)')

            if 'tracks' not in info:
                raise Exception('No track found in input file(s)')

            # select which KID/KEY to use for each track
            default_kid = options.key_infos[0]['kid']
            default_key = options.key_infos[0]['key']
            media_source.track_key_infos = {}
            for track in info['tracks']:
                for key_info in options.key_infos:
                    if track['type'].lower() in key_info['filter']:
                        media_source.track_key_infos[track['id']] = key_info

            # skip now if we're only outputing the MPD
            if options.no_media:
                continue

            # don't process any further if we won't have key material for this media source
            if len(media_source.track_key_infos) == 0:
                continue

            print 'Encrypting track IDs '+str(sorted(media_source.track_key_infos.keys()))+' in '+ GetMappedFileName(media_file)
            encrypted_file = tempfile.NamedTemporaryFile(dir = options.output_dir, delete=False)
            encrypted_files[media_file] = encrypted_file
            TempFiles.append(encrypted_file.name)
            encrypted_file.close() # necessary on Windows
            MapFileName(encrypted_file.name, path.basename(encrypted_file.name) + ' = Encrypted[' + GetMappedFileName(media_file) + ']')
            args = ['--method', MpegCencSchemeMap[options.encryption_cenc_scheme]]

            if options.encryption_args:
                args += options.encryption_args.split()
            else:
                if options.smooth or options.playready:
                    args += ['--global-option', 'mpeg-cenc.piff-compatible:true']

            for track_id in sorted(media_source.track_key_infos.keys()):
                key_info = media_source.track_key_infos[track_id]
                args += ['--key', str(track_id)+':'+key_info['key']+':'+key_info['iv'], '--property', str(track_id)+':KID:'+key_info['kid']]

            # EME Common Encryption / Clearkey
            if options.eme_signaling == 'pssh-v0':
                args += ['--pssh', EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID+':']
            elif options.eme_signaling == 'pssh-v1':
                args += ['--pssh-v1', EME_COMMON_ENCRYPTION_PSSH_SYSTEM_ID+':']

            # Marlin
            if options.marlin_add_pssh:
                marlin_pssh = ComputeMarlinPssh(options)
                pssh_file = tempfile.NamedTemporaryFile(dir = options.output_dir, delete=False)
                pssh_file.write(marlin_pssh)
                TempFiles.append(pssh_file.name)
                pssh_file.close() # necessary on Windows
                args += ['--pssh', MARLIN_PSSH_SYSTEM_ID+':'+pssh_file.name]

            # PlayReady
            if options.playready_add_pssh:
                playready_header = ComputePlayReadyHeader(options.playready_header, default_kid, default_key)
                pssh_file = tempfile.NamedTemporaryFile(dir = options.output_dir, delete=False)
                pssh_file.write(playready_header)
                TempFiles.append(pssh_file.name)
                pssh_file.close() # necessary on Windows
                args += ['--pssh', PLAYREADY_PSSH_SYSTEM_ID+':'+pssh_file.name]

            # Widevine
            if options.widevine_header:
                widevine_header = ComputeWidevineHeader(options.widevine_header, default_kid)
                pssh_file = tempfile.NamedTemporaryFile(dir = options.output_dir, delete=False)
                pssh_file.write(widevine_header)
                TempFiles.append(pssh_file.name)
                pssh_file.close() # necessary on Windows
                args += ['--pssh', WIDEVINE_PSSH_SYSTEM_ID+':'+pssh_file.name]

            # Primetime
            if options.primetime_metadata:
                primetime_metadata = ComputePrimetimeMetaData(options.primetime_metadata, default_kid)
                pssh_file = tempfile.NamedTemporaryFile(dir = options.output_dir, delete=False)
                pssh_file.write(primetime_metadata)
                TempFiles.append(pssh_file.name)
                pssh_file.close() # necessary on Windows
                args += ['--pssh', PRIMETIME_PSSH_SYSTEM_ID+':'+pssh_file.name]

            Mp4Encrypt(options, media_file, encrypted_file.name, *args)
            media_source.filename = encrypted_file.name

    # parse the media sources and select the audio and video tracks
    (audio_sets, video_sets, subtitles_sets, mp4_files) = SelectTracks(options, media_sources)
    subtitles_files = SelectSubtitlesFiles(options, media_sources)

    # store lists of all tracks by type
    audio_tracks     = sum(audio_sets.values(),     [])
    video_tracks     = sum(video_sets.values(),     [])
    subtitles_tracks = sum(subtitles_sets.values(), [])

    # check that we have at least one audio and one video
    if len(audio_tracks) == 0 and len(video_tracks) == 0 and len(subtitles_tracks) == 0:
        PrintErrorAndExit('ERROR: no track selected')

    if Options.verbose:
        print 'Audio:',     audio_sets
        print 'Video:',     video_sets
        print 'Subtitles:', subtitles_sets

    # assign key info to tracks
    if options.encryption_key:
        for track in audio_tracks+video_tracks+subtitles_tracks:
            track.key_info = track.parent.media_source.track_key_infos.get(track.id)

    # check that segments are consistent between tracks of the same adaptation set
    for tracks in video_sets.values():
        prev_track = None
        for track in tracks:
            if prev_track:
                if track.total_sample_count != prev_track.total_sample_count:
                    sys.stderr.write('WARNING: video sample count mismatch between "'+str(track)+'" and "'+str(prev_track)+'"\n')
            prev_track = track

    # check that the video segments match for all video tracks in the same adaptation set
    for tracks in video_sets.values():
        if len(tracks) > 1:
            anchor = tracks[0]
            for track in tracks[1:]:
                if track.sample_counts[:-1] != anchor.sample_counts[:-1]:
                    PrintErrorAndExit('ERROR: video tracks are not aligned ("'+str(track)+'" differs from '+str(anchor)+')')

    # check that the video segment durations are almost all equal
    if not options.use_segment_timeline:
        for video_track in video_tracks:
            for segment_duration in video_track.segment_durations[:-2]:
                ratio = segment_duration/video_track.average_segment_duration
                if ratio > 1.1 or ratio < 0.9:
                    sys.stderr.write('WARNING: video segment durations for "' + str(video_track) + '" vary by more than 10% (consider using --use-segment-timeline)\n')
                    break
        for audio_track in audio_tracks:
            for segment_duration in audio_track.segment_durations[:-2]:
                ratio = segment_duration/audio_track.average_segment_duration
                if ratio > 1.1 or ratio < 0.9:
                    sys.stderr.write('WARNING: audio segment durations for "' + str(audio_track) + '" vary by more than 10% (consider using --use-segment-timeline)\n')
                    break

    # round the audio segment durations to be equal to the video segment durations
    if len(video_tracks):
        for audio_track in audio_tracks:
            ratio = audio_track.average_segment_duration/video_tracks[0].average_segment_duration
            if abs(ratio-1.0) < 0.05:
                # within 5%, make it equal
                if options.verbose:
                    print 'INFO: adjusting segment duration for audio track '+str(audio_track)+' to '+str(video_tracks[0].average_segment_duration)+' to match the video'
                audio_track.average_segment_duration = video_tracks[0].average_segment_duration

    # compute the representation id and init segment name for each track
    for adaptation_sets in [audio_sets, video_sets, subtitles_sets]:
        for adaptation_set_name, tracks in adaptation_sets.items():
            for track in tracks:
                if options.split:
                    track.representation_id = '/'.join(adaptation_set_name)
                    if len(tracks) > 1:
                        track.representation_id += '/'+str(track.order_index)
                    track.init_segment_name = SPLIT_INIT_SEGMENT_NAME
                else:
                    track.representation_id = '-'.join(adaptation_set_name)
                    if len(tracks) > 1:
                        track.representation_id += '-'+str(track.order_index)
                    if options.on_demand:
                        track.parent.media_name = ONDEMAND_MEDIA_FILE_PATTERN % (options.media_prefix, track.representation_id)
                    else:
                        track.init_segment_name = NOSPLIT_INIT_FILE_PATTERN % (track.representation_id)
                track.stream_id = adaptation_set_name[0]
                if adaptation_set_name[0] == 'audio':
                    track.stream_id += '_'+track.language

    # compute index and init offsets for the on-demand profile
    if options.on_demand:
        for track in audio_tracks+video_tracks+subtitles_tracks:
            atoms = WalkAtoms(track.parent.media_source.filename, 'moof')
            for atom in atoms:
                if atom.type == 'sidx' and not hasattr(track, 'sidx_atom'):
                    track.sidx_atom = atom
                if atom.type == 'moov' and not hasattr(track, 'moov_atom'):
                    track.moov_atom = atom

    # compute some values if not set
    if options.min_buffer_time == 0.0:
        if len(video_tracks):
            options.min_buffer_time = video_tracks[0].average_segment_duration
        else:
            options.min_buffer_time = audio_tracks[0].average_segment_duration

    # print info about the tracks
    if options.verbose:
        for track in audio_tracks+video_tracks+subtitles_tracks:
            print ('%s track: ' + str(track) + ' - language=%s, max bitrate=%d, avg bitrate=%d, req bandwidth=%d, codec=%s') % (track.type, track.language, track.max_segment_bitrate, track.average_segment_bitrate, track.bandwidth, track.codec)

    # deal with the max playout strategy if set
    if options.max_playout_rate_strategy:
        max_playout_rate = options.max_playout_rate_strategy.split(':')[1]
        lowest_bandwidth_track = None
        lowest_bandwidth = -1
        for video_track in video_tracks:
            if lowest_bandwidth < 0 or video_track.bandwidth < lowest_bandwidth:
                lowest_bandwidth = video_track.bandwidth
                lowest_bandwidth_track = video_track
        if lowest_bandwidth_track:
            lowest_bandwidth_track.max_playout_rate = max_playout_rate

    # create the directories and split/copy/process the media if needed
    if not options.no_media:
        if options.split:
            for adaptation_sets in [audio_sets, video_sets, subtitles_sets]:
                for adaptation_set_name, tracks in adaptation_sets.items():
                    base_dir = options.output_dir
                    for subdir in adaptation_set_name:
                        base_dir = path.join(base_dir, subdir)
                        MakeNewDir(base_dir)
                    for track in tracks:
                        out_dir = base_dir
                        if len(tracks) > 1:
                            out_dir = path.join(out_dir, str(track.order_index))
                            MakeNewDir(out_dir)
                        print 'Splitting media file ('+adaptation_set_name[0]+')', GetMappedFileName(track.parent.media_source.filename)
                        Mp4Split(options,
                                 track.parent.media_source.filename,
                                 track_id               = str(track.id),
                                 pattern_parameters     = 'N',
                                 start_number           = '1',
                                 init_segment           = path.join(out_dir, track.init_segment_name),
                                 media_segment          = path.join(out_dir, SEGMENT_PATTERN))

            # if len(video_tracks):
            #     MakeNewDir(path.join(options.output_dir, 'video'))
            # for video_track in video_tracks:
            #     out_dir = path.join(options.output_dir, 'video', str(video_track.order_index))
            #     MakeNewDir(out_dir)
            #     print 'Splitting media file (video)', GetMappedFileName(video_track.parent.media_source.filename)
            #     Mp4Split(Options,
            #              video_track.parent.media_source.filename,
            #              track_id               = str(video_track.id),
            #              pattern_parameters     = 'N',
            #              start_number           = '1',
            #              init_segment           = path.join(out_dir, video_track.init_segment_name),
            #              media_segment          = path.join(out_dir, SEGMENT_PATTERN))
            #
            # for adaptation_set_name, tracks in subtitles_sets.items():
            #     out_dir = options.output_dir
            #     for subdir in adaptation_set_name:
            #         out_dir = path.join(out_dir, subdir)
            #         MakeNewDir(out_dir)
            #     for subtitles_track in tracks:
            #         print 'Splitting media file (subtitles)', GetMappedFileName(subtitles_track.parent.media_source.filename)
            #         Mp4Split(Options,
            #                  subtitles_track.parent.media_source.filename,
            #                  track_id               = str(subtitles_track.id),
            #                  pattern_parameters     = 'N',
            #                  start_number           = '1',
            #                  init_segment           = path.join(out_dir, subtitles_track.init_segment_name),
            #                  media_segment          = path.join(out_dir, SEGMENT_PATTERN))
        else:
            for mp4_file in mp4_files.values():
                print 'Processing and Copying media file', GetMappedFileName(mp4_file.media_source.filename)
                media_filename = path.join(options.output_dir, mp4_file.media_name)
                if not options.force_output and path.exists(media_filename):
                    PrintErrorAndExit('ERROR: file ' + media_filename + ' already exists')

                shutil.copyfile(mp4_file.media_source.filename, media_filename)
            if options.smooth or options.hippo:
                for track in audio_tracks+video_tracks+subtitles_tracks:
                    Mp4Split(options,
                             track.parent.media_source.filename,
                             track_id     = str(track.id),
                             init_only    = True,
                             init_segment = path.join(options.output_dir, track.init_segment_name))

        if len(subtitles_files):
            MakeNewDir(path.join(options.output_dir, 'subtitles'))
            for subtitles_file in subtitles_files:
                print 'Processing and Copying subtitles file', GetMappedFileName(subtitles_file.media_source.filename)
                out_dir = path.join(options.output_dir, 'subtitles', subtitles_file.language)
                MakeNewDir(out_dir)
                media_filename = path.join(out_dir, subtitles_file.media_name)
                shutil.copyfile(subtitles_file.media_source.filename, media_filename)

    # output the DASH MPD
    OutputDash(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files)

    # output the HLS playlists
    if options.hls:
        OutputHls(options, set_attributes, audio_sets, video_sets, subtitles_sets, subtitles_files)

    # output the Smooth Manifests
    if options.smooth:
        OutputSmooth(options, audio_tracks, video_tracks)

    # output the Hippo Manifest
    if options.hippo:
        OutputHippo(options, audio_tracks, video_tracks)

###########################
if sys.version_info[0] != 2:
    sys.stderr.write("ERROR: This tool must be run with Python 2.x\n")
    sys.stderr.write("You are running Python version: "+sys.version+"\n")
    exit(1)

if __name__ == '__main__':
    try:
        main()
    except Exception, err:
        if Options and Options.debug:
            raise
        else:
            PrintErrorAndExit('ERROR: %s\n' % str(err))
    finally:
        for f in TempFiles:
            os.unlink(f)

Zerion Mini Shell 1.0