#! /usr/bin/env python3

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

###
# NOTE: this script needs Bento4 command line binaries to run
# You must place the 'mp4info' 'mp4dump', and 'mp42hls' 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 platform
import sys
import os.path as path
import json
from subtitles import SubtitlesFile
from mp4utils import Base64Encode,\
                     Mp4File,\
                     Mp42Hls,\
                     MediaSource,\
                     LanguageNames,\
                     LanguageCodeMap,\
                     PrintErrorAndExit,\
                     MakeNewDir

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

#############################################
def CreateSubtitlesPlaylist(playlist_filename, webvtt_filename, duration):
    playlist = open(playlist_filename, 'w', newline='\r\n')
    playlist.write('#EXTM3U\n')
    playlist.write('#EXT-X-TARGETDURATION:%d\n' % (duration))
    playlist.write('#EXT-X-VERSION:3\n')
    playlist.write('#EXT-X-MEDIA-SEQUENCE:0\n')
    playlist.write('#EXT-X-PLAYLIST-TYPE:VOD\n')
    playlist.write('#EXTINF:%d,\n' % (duration))
    playlist.write(webvtt_filename+'\n')
    playlist.write('#EXT-X-ENDLIST\n')


#############################################
def ComputeCodecName(codec_family):
    name = codec_family
    if codec_family == 'mp4a':
        name = 'aac'
    elif codec_family == 'ac-3':
        name = 'ac3'
    elif codec_family == 'ec-3':
        name = 'ec3'
    return name

#############################################
def SplitArgs(args):
    try:
        pairs = args.split('#')
        fields = {}
        for pair in pairs:
            name, value = pair.split(':', 1)
            fields[name] = value
        return fields
    except:
        raise Exception('invalid syntax for argument')

#############################################
def ComputeWidevineKeyLine(params):
    if type(params) == str:
        # already in base64 form
        base64_header = params
    else:
        # base64-encode a JSON object for the parameters
        base64_header = Base64Encode(('{ "provider": "%(provider)s", "content_id": "%(content_id)s", "key_ids": ["%(kid)s"] }' % params).encode('ascii'))
    key_line   = 'URI="data:text/plain;base64,' + base64_header + '",KEYFORMAT="com.widevine",KEYFORMATVERSIONS="1"'

    return key_line

#############################################
def ComputeFairplayKeyLine(params):
    # start with a '!' to specify we want to skip the IV (since it is not needed on the key line for Fairplay)
    return '!URI="'+params['uri']+'",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"'

#############################################
def AnalyzeSources(options, media_sources):
    # parse the media files
    mp4_files = {}
    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 path.exists(media_file):
            PrintErrorAndExit('ERROR: media file ' + media_file + ' does not exist')

        # get the file info
        print('Parsing media file', media_file)
        mp4_file = Mp4File(Options, media_source)
        media_source.mp4_file = mp4_file

        # remember we have parsed this file
        mp4_files[media_file] = mp4_file

    # analyze the media sources
    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_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:
            for track in list(media_source.mp4_file.tracks.values()):
                language = LanguageCodeMap.get(track.language, track.language)
                if track_language and track_language != language and track_language != track.language:
                    continue
                tracks.append(track)

        # remember if this media source has a video or audio track
        for track in tracks:
            if track.type == 'video':
                media_source.has_video = True
            if track.type == 'audio':
                media_source.has_audio = True

        media_source.tracks = tracks

#############################################
def SelectAudioTracks(options, media_sources):
    # select tracks grouped by codec
    audio_tracks = {}
    for media_source in media_sources:

        # pre-process the track metadata
        for track in media_source.tracks:
            # track group
            track.group_id = ComputeCodecName(track.codec_family)

            # track language
            remap_language = media_source.spec.get('+language')
            if remap_language:
                track.language = remap_language
            language_name = LanguageNames.get(track.language, track.language)
            track.language_name = media_source.spec.get('+language_name', language_name)

        # process audio tracks
        for track in [t for t in media_source.tracks if t.type == 'audio']:
            group_id = track.group_id
            group = audio_tracks.get(group_id, [])
            audio_tracks[group_id] = group
            if len([x for x in group if x.language == track.language]):
                continue # only accept one track for each language per group
            group.append(track)

    return audio_tracks

#############################################
def ProcessSource(options, media_info, out_dir):
    if options.verbose:
        print('Processing', media_info['source'].filename)

    file_extension = media_info.get('file_extension', 'ts')

    kwargs = {
        'index_filename':            path.join(out_dir, options.media_playlist_name),
        'segment_filename_template': path.join(out_dir, 'segment-%d.'+file_extension),
        'segment_url_template':      'segment-%d.'+file_extension,
        'show_info':                 True
    }

    if options.base_url != "":
        kwargs["segment_url_template"] = options.base_url+media_info["dir"]+'/'+'segment-%d.'+file_extension

    if options.hls_version != 3:
        kwargs['hls_version'] = str(options.hls_version)

    if options.hls_version >= 4:
        kwargs['iframe_index_filename'] = path.join(out_dir, options.iframe_playlist_name)

    if options.output_single_file:
        kwargs['segment_filename_template'] = path.join(out_dir, 'media.'+file_extension)
        kwargs['segment_url_template']      = 'media.'+file_extension
        kwargs['output_single_file']        = True

    if 'audio_format' in media_info and media_info.get('audio_track_id') != 0:
        kwargs['audio_format'] = media_info['audio_format']

    for option in ['encryption_mode', 'encryption_key', 'encryption_iv_mode', 'encryption_key_uri', 'encryption_key_format', 'encryption_key_format_versions']:
        if getattr(options, option):
            kwargs[option] = getattr(options, option)

    key_lines = []

    # Fairplay
    if options.fairplay:
        key_lines.append(ComputeFairplayKeyLine(options.fairplay))

    # Widevine
    if options.widevine:
        key_lines.append(ComputeWidevineKeyLine(options.widevine))

    if len(key_lines):
        kwargs['encryption_key_line'] = key_lines

    # deal with track IDs
    if 'audio_track_id' in media_info:
        kwargs['audio_track_id'] = str(media_info['audio_track_id'])
    if 'video_track_id' in media_info:
        kwargs['video_track_id'] = str(media_info['video_track_id'])

    # other options
    if options.segment_duration:
        kwargs['segment_duration'] = options.segment_duration

    # convert to HLS/TS
    json_info = Mp42Hls(options,
                        media_info['source'].filename,
                        **kwargs)
    if options.verbose:
        print(json_info.decode('utf-8'))

    media_info['info'] = json.loads(json_info, strict=False)

    # output the encryption key if needed
    if options.output_encryption_key:
        open(path.join(out_dir, 'key.bin'), 'wb').write(bytes.fromhex(options.encryption_key)[:16])

#############################################
def OutputHls(options, media_sources):
    mp4_sources = [media_source for media_source in media_sources if media_source.format == 'mp4']

    # analyze the media sources
    AnalyzeSources(options, media_sources)

    # select audio tracks
    audio_tracks = SelectAudioTracks(options, [media_source for media_source in mp4_sources if not media_source.spec.get('+audio_fallback')])

    # check if this is an audio-only presentation
    audio_only = True
    for media_source in mp4_sources:
        if media_source.has_video:
            audio_only = False
            break

    # check if the video has muxed audio
    video_has_muxed_audio = False
    for media_source in mp4_sources:
        if media_source.has_video and media_source.has_audio:
            video_has_muxed_audio = True
            break

    # audio-only presentations don't need alternate audio tracks
    if audio_only:
        audio_tracks = {}

    # we only need alternate audio tracks if there are more than one or if the audio and video are not muxed
    if video_has_muxed_audio and not audio_only and len(audio_tracks) == 1 and len(list(audio_tracks.values())[0]) == 1:
        audio_tracks = {}

    # process main media sources
    total_duration = 0
    main_media = []
    for media_source in mp4_sources:
        if not audio_only and not media_source.spec.get('+audio_fallback') and not media_source.has_video:
            continue
        media_index = 1+len(main_media)
        media_info = {
            'source':      media_source,
            'dir':         'media-'+str(media_index)
        }
        if audio_only:
            media_info['video_track_id'] = 0
            if options.audio_format == 'packed':
                source_audio_tracks = media_source.mp4_file.find_tracks_by_type('audio')
                if len(source_audio_tracks):
                    media_info['audio_format']   = options.audio_format
                    if options.audio_format == 'packed':
                        media_info['file_extension'] = ComputeCodecName(source_audio_tracks[0].codec_family)

        # no audio if there's a type filter for video
        if media_source.spec.get('type') == 'video':
            media_info['audio_track_id'] = 0

        # deal with audio-fallback streams
        if media_source.spec.get('+audio_fallback') == 'yes':
            media_info['video_track_id'] = 0

        # process the source
        out_dir = path.join(options.output_dir, media_info['dir'])
        MakeNewDir(out_dir)
        ProcessSource(options, media_info, out_dir)

        # update the duration
        duration_s = int(media_info['info']['stats']['duration'])
        if duration_s > total_duration:
            total_duration = duration_s

        main_media.append(media_info)

    # process audio tracks
    if len(audio_tracks):
        MakeNewDir(path.join(options.output_dir, 'audio'))
    if options.audio_format == 'ts':
        for audios in audio_tracks.values():
            for audio in audios:
                if audio.codec_family in ['ec-3'] and audio.dolby_ddp_atmos == 'Yes':
                    PrintErrorAndExit('ERROR: For Dolby Digital Plus with Dolby Atmos, the format of segment audio cannot be MPEG2TS, please add "--audio-format packed"')
    for group_id in audio_tracks:
        group = audio_tracks[group_id]
        MakeNewDir(path.join(options.output_dir, 'audio', group_id))
        for audio_track in group:
            audio_track.media_info = {
                'source':         audio_track.parent.media_source,
                'audio_format':   options.audio_format,
                'dir':            'audio/'+group_id+'/'+audio_track.language,
                'language':       audio_track.language,
                'language_name':  audio_track.language_name,
                'audio_track_id': audio_track.id,
                'video_track_id': 0
            }
            if options.audio_format == 'packed':
                audio_track.media_info['file_extension'] = ComputeCodecName(audio_track.codec_family)

            # process the source
            out_dir = path.join(options.output_dir, 'audio', group_id, audio_track.language)
            MakeNewDir(out_dir)
            ProcessSource(options, audio_track.media_info, out_dir)

    # start the master playlist
    master_playlist = open(path.join(options.output_dir, options.master_playlist_name), 'w', newline='\r\n')
    master_playlist.write('#EXTM3U\n')
    master_playlist.write('# Created with Bento4 mp4-hls.py version '+VERSION+'r'+SDK_REVISION+'\n')

    if options.hls_version >= 4:
        master_playlist.write('\n')
        master_playlist.write('#EXT-X-VERSION:'+str(options.hls_version)+'\n')

    # optional session key
    if options.signal_session_key:
        ext_x_session_key_line = '#EXT-X-SESSION-KEY:METHOD='+options.encryption_mode+',URI="'+options.encryption_key_uri+'"'
        if options.encryption_key_format:
            ext_x_session_key_line += ',KEYFORMAT="'+options.encryption_key_format+'"'
        if options.encryption_key_format_versions:
            ext_x_session_key_line += ',KEYFORMATVERSIONS="'+options.encryption_key_format_versions+'"'
        master_playlist.write(ext_x_session_key_line+'\n')

    # process subtitles sources
    subtitles_files = [SubtitlesFile(options, media_source) for media_source in media_sources if media_source.format in ['ttml', 'webvtt']]
    if len(subtitles_files):
        master_playlist.write('\n')
        master_playlist.write('# Subtitles\n')
        MakeNewDir(path.join(options.output_dir, 'subtitles'))
        for subtitles_file in subtitles_files:
            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)
            relative_url = 'subtitles/'+subtitles_file.language+'/subtitles.m3u8'
            playlist_filename = path.join(out_dir, 'subtitles.m3u8')
            CreateSubtitlesPlaylist(playlist_filename, subtitles_file.media_name, total_duration)

            master_playlist.write('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="%s",LANGUAGE="%s",URI="%s"\n' % (subtitles_file.language_name, subtitles_file.language, relative_url))

    # process audio sources
    audio_groups = []
    if len(audio_tracks):
        master_playlist.write('\n')
        master_playlist.write('# Audio\n')
        for group_id in audio_tracks:
            group = audio_tracks[group_id]
            group_name = 'audio_'+group_id
            group_codec = group[0].codec
            default = True
            group_avg_segment_bitrate = 0
            group_max_segment_bitrate = 0
            for audio_track in group:
                avg_segment_bitrate = int(audio_track.media_info['info']['stats']['avg_segment_bitrate'])
                max_segment_bitrate = int(audio_track.media_info['info']['stats']['max_segment_bitrate'])
                if avg_segment_bitrate > group_avg_segment_bitrate:
                    group_avg_segment_bitrate = avg_segment_bitrate
                if max_segment_bitrate > group_max_segment_bitrate:
                    group_max_segment_bitrate = max_segment_bitrate
                extra_info = 'AUTOSELECT=YES,'
                if default:
                    extra_info += 'DEFAULT=YES,'
                    default = False
                master_playlist.write(('#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="%s",NAME="%s",LANGUAGE="%s",CHANNELS="%s",%sURI="%s"\n' % (
                                      group_name,
                                      audio_track.media_info['language_name'],
                                      audio_track.media_info['language'],
                                      str(audio_track.channels),
                                      extra_info,
                                      options.base_url+audio_track.media_info['dir']+'/'+options.media_playlist_name)))
            audio_groups.append({
                'name':                group_name,
                'codec':               group_codec,
                'avg_segment_bitrate': group_avg_segment_bitrate,
                'max_segment_bitrate': group_max_segment_bitrate
            })

        if options.debug:
            print('Audio Groups:')
            print(audio_groups)

    else:
        audio_groups = [{
            'name':                None,
            'codec':               None,
            'avg_segment_bitrate': 0,
            'max_segment_bitrate': 0
        }]

    # media playlists
    master_playlist.write('\n')
    master_playlist.write('# Media Playlists\n')
    for media in main_media:
        media_info = media['info']

        for group_info in audio_groups:
            group_name  = group_info['name']
            group_codec = group_info['codec']

            # stream inf
            codecs = []
            if 'video' in media_info:
                codecs.append(media_info['video']['codec'])
            if 'audio' in media_info:
                codecs.append(media_info['audio']['codec'])
            elif group_name and group_codec:
                codecs.append(group_codec)

            ext_x_stream_inf = '#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=%d,BANDWIDTH=%d,CODECS="%s"' % (
                                int(media_info['stats']['avg_segment_bitrate'])+group_info['avg_segment_bitrate'],
                                int(media_info['stats']['max_segment_bitrate'])+group_info['max_segment_bitrate'],
                                ','.join(codecs))
            if 'video' in media_info:
                ext_x_stream_inf += ',RESOLUTION='+str(int(media_info['video']['width']))+'x'+str(int(media_info['video']['height']))

            # audio info
            if group_name:
                ext_x_stream_inf += ',AUDIO="'+group_name+'"'

            # subtitles info
            if subtitles_files:
                ext_x_stream_inf += ',SUBTITLES="subtitles"'

            master_playlist.write(ext_x_stream_inf+'\n')
            master_playlist.write(options.base_url+media['dir']+'/'+options.media_playlist_name+'\n')

    # write the I-FRAME playlist info
    if not audio_only and options.hls_version >= 4:
        master_playlist.write('\n')
        master_playlist.write('# I-Frame Playlists\n')
        for media in main_media:
            media_info = media['info']
            if not 'video' in media_info: continue
            ext_x_i_frame_stream_inf = '#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=%d,BANDWIDTH=%d,CODECS="%s",RESOLUTION=%dx%d,URI="%s"' % (
                                        int(media_info['stats']['avg_iframe_bitrate']),
                                        int(media_info['stats']['max_iframe_bitrate']),
                                        media_info['video']['codec'],
                                        int(media_info['video']['width']),
                                        int(media_info['video']['height']),
                                        options.base_url+media['dir']+'/'+options.iframe_playlist_name)
            master_playlist.write(ext_x_i_frame_stream_inf+'\n')

#############################################
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')
    if not path.exists(default_exec_dir):
        default_exec_dir = '-'

    # parse options
    parser = OptionParser(usage="%prog [options] <media-file> [<media-file> ...]",
                          description="Each <media-file> is the path to an 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", default=False,
                      help="Allow output to an existing directory")
    parser.add_option('', '--hls-version', dest="hls_version", type="int", metavar="<version>", default=4,
                      help="HLS Version (default: 4)")
    parser.add_option('', '--master-playlist-name', dest="master_playlist_name", metavar="<filename>", default='master.m3u8',
                      help="Master Playlist name")
    parser.add_option('', '--media-playlist-name', dest="media_playlist_name", metavar="<name>", default='stream.m3u8',
                      help="Media Playlist name")
    parser.add_option('', '--iframe-playlist-name', dest="iframe_playlist_name", metavar="<name>", default='iframes.m3u8',
                      help="I-frame Playlist name")
    parser.add_option('', '--output-single-file', dest="output_single_file", action='store_true', default=False,
                      help="Store segment data in a single output file per input file")
    parser.add_option('', '--audio-format', dest="audio_format", default='packed',
                      help="Format for audio segments (packed or ts) (default: packed)")
    parser.add_option('', '--segment-duration', dest="segment_duration",
                      help="Segment duration (default: 6)")
    parser.add_option('', '--encryption-mode', dest="encryption_mode", metavar="<mode>",
                      help="Encryption mode (only used when --encryption-key is specified). AES-128 or SAMPLE-AES (default: AES-128)")
    parser.add_option('', '--encryption-key', dest="encryption_key", metavar="<key>",
                      help="Encryption key in hexadecimal (default: no encryption)")
    parser.add_option('', '--encryption-iv-mode', dest="encryption_iv_mode", metavar="<mode>",
                      help="Encryption IV mode: 'sequence', 'random' or 'fps' (Fairplay Streaming) (default: sequence). When the mode is 'fps', the encryption key must be 32 bytes: 16 bytes for the key followed by 16 bytes for the IV.")
    parser.add_option('', '--encryption-key-uri', dest="encryption_key_uri", metavar="<uri>", default="key.bin",
                      help="Encryption key URI (may be a relative or absolute URI). (default: key.bin)")
    parser.add_option('', '--encryption-key-format', dest="encryption_key_format", metavar="<format>",
                      help="Encryption key format. (default: 'identity')")
    parser.add_option('', '--encryption-key-format-versions', dest="encryption_key_format_versions", metavar="<versions>",
                      help="Encryption key format versions.")
    parser.add_option('', '--signal-session-key', dest='signal_session_key', action='store_true', default=False,
                      help="Signal an #EXT-X-SESSION-KEY tag in the master playlist")
    parser.add_option('', '--fairplay', dest="fairplay", metavar="<fairplay-parameters>",
                      help="Enable Fairplay Key Delivery. " +
                           "The <fairplay-parameters> argument is one or more <name>:<value> pair(s) (separated by '#' if more than one). " +
                           "Names include 'uri' [string] (required)")
    parser.add_option('', '--widevine', dest="widevine", metavar="<widevine-parameters>",
                      help="Enable Widevine Key Delivery. " +
                           "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] (required), 'content_id' [byte array in hex] (optional), 'kid' [16-byte array in hex] (required))")
    parser.add_option('', '--output-encryption-key', dest="output_encryption_key", action="store_true", default=False,
                      help="Output the encryption key to a file (default: don't output the key). This option is only valid when the encryption key format is 'identity'")
    parser.add_option('', "--exec-dir", metavar="<exec_dir>", dest="exec_dir", default=default_exec_dir,
                      help="Directory where the Bento4 executables are located")
    parser.add_option('', "--base-url", metavar="<base_url>", dest="base_url", default="",
                      help="The base URL for the Media Playlists and TS files listed in the playlists. This is the prefix for the files.")
    (options, args) = parser.parse_args()
    if len(args) == 0:
        parser.print_help()
        sys.exit(1)
    global Options
    Options = options

    # set some mandatory options that utils rely upon
    options.min_buffer_time = 0.0

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

    # check options
    if options.output_encryption_key:
        if options.encryption_key_uri != "key.bin":
            sys.stderr.write("WARNING: the encryption key will not be output because a non-default key URI was specified\n")
            options.output_encryption_key = False
        if not options.encryption_key:
            sys.stderr.write("ERROR: --output-encryption-key requires --encryption-key to be specified\n")
            sys.exit(1)
        if options.encryption_key_format != None and options.encryption_key_format != 'identity':
            sys.stderr.write("ERROR: --output-encryption-key requires --encryption-key-format to be omitted or set to 'identity'\n")
            sys.exit(1)

    # Fairplay option
    if options.fairplay:
        if not options.encryption_key_format:
            options.encryption_key_format = 'com.apple.streamingkeydelivery'
        if not options.encryption_key_format_versions:
            options.encryption_key_format_versions = '1'

        if options.encryption_iv_mode:
            if options.encryption_iv_mode != 'fps':
                sys.stderr.write("ERROR: --fairplay requires --encryption-iv-mode to be 'fps'\n")
                sys.exit(1)
        else:
            options.encryption_iv_mode = 'fps'
        if not options.encryption_key:
            sys.stderr.write("ERROR: --fairplay requires --encryption-key to be specified\n")
            sys.exit(1)
        if options.encryption_mode:
            if options.encryption_mode != 'SAMPLE-AES':
                sys.stderr.write('ERROR: --fairplay option incompatible with '+options.encryption_mode+' encryption mode\n')
                sys.exit(1)
        else:
            options.encryption_mode = 'SAMPLE-AES'
        options.fairplay = SplitArgs(options.fairplay)
        if 'uri' not in options.fairplay:
            sys.stderr.write('ERROR: --fairplay option requires a "uri" parameter (ex: skd://xxx)\n')
            sys.exit(1)

        options.signal_session_key = True

    # Widevine option
    if options.widevine:
        if not options.encryption_key:
            sys.stderr.write("ERROR: --widevine requires --encryption-key to be specified\n")
            sys.exit(1)
        if options.encryption_mode:
            if options.encryption_mode != 'SAMPLE-AES':
                sys.stderr.write('ERROR: --widevine option incompatible with '+options.encryption_mode+' encryption mode\n')
                sys.exit(1)
        else:
            options.encryption_mode = 'SAMPLE-AES'

        if options.widevine.startswith('#'):
            options.widevine = options.widevine[1:]
        else:
            options.widevine = SplitArgs(options.widevine)
            if 'kid' not in options.widevine:
                sys.stderr.write('ERROR: --widevine option requires a "kid" parameter\n')
                sys.exit(1)
            if len(options.widevine['kid']) != 32:
                sys.stderr.write('ERROR: --widevine option "kid" must be 32 hex characters\n')
                sys.exit(1)
            if 'provider' not in options.widevine:
                sys.stderr.write('ERROR: --widevine option requires a "provider" parameter\n')
                sys.exit(1)
            if 'content_id' in options.widevine:
                options.widevine['content_id'] = bytes.fromhex(options.widevine['content_id'])
            else:
                options.widevine['content_id'] = '*'

    # defaults
    if options.encryption_key and not options.encryption_mode:
        options.encryption_mode = 'AES-128'

    if options.encryption_mode == 'SAMPLE-AES':
        options.hls_version = 5

    # parse media sources syntax
    media_sources = [MediaSource(options, source) for source in args]
    for media_source in media_sources:
        media_source.has_audio  = False
        media_source.has_video  = False

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

    # output the media playlists
    OutputHls(options, media_sources)

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

if __name__ == '__main__':
    try:
        main()
    except Exception as err:
        if Options and Options.debug:
            raise
        else:
            PrintErrorAndExit('ERROR: %s\n' % str(err))
