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)
def main(): # parse options global Options parser = OptionParser( usage="%prog [options] <media-file>", description="<media-file> is the path to a source video file. Version " + VERSION + " r" + SVN_REVISION[-5:-2]) 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('-k', '--keep-files', dest="keep_files", action='store_true', default=False, help="Keep intermediate files") parser.add_option('-o', '--output-dir', dest="output_dir", help="Output directory", metavar="<output-dir>", default='') parser.add_option('-b', '--bitrates', dest="bitrates", help="Number of bitrates (default: 1)", default=1, type='int') parser.add_option( '-r', '--resolution', dest='resolution', help="Resolution of the source video (default: auto detect)") parser.add_option('-m', '--min-video-bitrate', dest='min_bitrate', type='float', help="Minimum bitrate (default: 500kbps)", default=500.0) parser.add_option('-n', '--max-video-bitrate', dest='max_bitrate', type='float', help="Max Video bitrate (default: 2mbps)", default=2000.0) parser.add_option('--audio-codec', dest='audio_codec', default='libfdk_aac', help='Audio Codec: libfdk_aac (default) or aac') parser.add_option('-c', '--video-codec', dest='video_codec', default='libx264', help="Video Codec: libx264 (default) or libx265") parser.add_option('-a', '--audio-bitrate', dest='audio_bitrate', type='int', help="Audio bitrate (default: 128kbps)", default=128) parser.add_option( '', '--select-streams', dest='select_streams', help= "Only encode these streams (comma-separated list of stream indexes or stream specifiers)" ) parser.add_option('-s', '--segment-size', dest='segment_size', type='int', help="Video segment size in frames (default: 3*fps)") parser.add_option('-t', '--text-overlay', dest='text_overlay', action='store_true', default=False, help="Add a text overlay with the bitrate") parser.add_option( '', '--text-overlay-font', dest='text_overlay_font', default=None, help="Specify a TTF font file to use for the text overlay") parser.add_option('-e', '--encoder-params', dest='encoder_params', help="Extra encoder parameters") parser.add_option('-f', '--force', dest="force_output", action="store_true", help="Overwrite output files if they already exist", default=False) (options, args) = parser.parse_args() Options = options if len(args) == 0: parser.print_help() sys.exit(1) if options.resolution: options.resolution = [int(x) for x in options.resolution.split('x')] if len(options.resolution) != 2: raise Exception('ERROR: invalid value for --resolution argument') if options.min_bitrate > options.max_bitrate: raise Exception('ERROR: max bitrate must be >= min bitrate') if options.output_dir: MakeNewDir(dir=options.output_dir, exit_if_exists=not (options.force_output), severity='ERROR') if options.verbose: print('Encoding', options.bitrates, 'bitrates, min bitrate =', options.min_bitrate, 'max bitrate =', options.max_bitrate) media_source = MediaSource(options, args[0]) if not options.resolution: options.resolution = [media_source.width, media_source.height] if options.verbose: print('Media Source:', media_source) if not options.segment_size: options.segment_size = 3 * int(media_source.frame_rate + 0.5) if options.bitrates == 1: options.min_bitrate = options.max_bitrate (bitrates, resolutions) = compute_bitrates_and_resolutions(options) for i in range(options.bitrates): output_filename = path.join(options.output_dir, 'video_%05d.mp4' % int(bitrates[i])) temp_filename = output_filename + '_' base_cmd = 'ffmpeg -i %s -strict experimental -codec:a %s -ac 2 -ab %dk -preset slow -map_metadata -1 -codec:v %s' % ( quote(args[0]), options.audio_codec, options.audio_bitrate, options.video_codec) if options.video_codec == 'libx264': base_cmd += ' -profile:v baseline' if options.text_overlay: if not options.text_overlay_font: font_file = "/Library/Fonts/Courier New.ttf" if path.exists(font_file): options.text_overlay_font = font_file else: raise Exception( 'ERROR: no default font file, please use the --text-overlay-font option' ) if not path.exists(options.text_overlay_font): raise Exception('ERROR: font file "' + options.text_overlay_font + '" does not exist') base_cmd += ' -vf "drawtext=fontfile=' + options.text_overlay_font + ': text=' + str( int(bitrates[i]) ) + 'kbps ' + str(resolutions[i][0]) + '*' + str( resolutions[i] [1]) + ': fontsize=50: x=(w)/8: y=h-(2*lh): fontcolor=white:"' if options.select_streams: specifiers = options.select_streams.split(',') for specifier in specifiers: base_cmd += ' -map 0:' + specifier else: base_cmd += ' -map 0' if not options.debug: base_cmd += ' -v quiet' if options.force_output: base_cmd += ' -y' #x264_opts = "-x264opts keyint=%d:min-keyint=%d:scenecut=0:rc-lookahead=%d" % (options.segment_size, options.segment_size, options.segment_size) #video_opts = "-g %d" % (options.segment_size) video_opts = "-force_key_frames 'expr:eq(mod(n,%d),0)'" % ( options.segment_size) video_opts += " -bufsize %dk -maxrate %dk" % (bitrates[i], int(bitrates[i] * 1.5)) if options.video_codec == 'libx264': video_opts += " -x264opts rc-lookahead=%d" % (options.segment_size) elif options.video_codec == 'libx265': video_opts += ' -x265-params "no-open-gop=1:keyint=%d:no-scenecut=1:profile=main"' % ( options.segment_size) if options.encoder_params: video_opts += ' ' + options.encoder_params cmd = base_cmd + ' ' + video_opts + ' -s ' + str( resolutions[i][0]) + 'x' + str( resolutions[i][1]) + ' -f mp4 ' + temp_filename if options.verbose: print('ENCODING bitrate: %d, resolution: %dx%d' % (int(bitrates[i]), resolutions[i][0], resolutions[i][1])) run_command(options, cmd) cmd = 'mp4fragment "%s" "%s"' % (temp_filename, output_filename) run_command(options, cmd) if not options.keep_files: os.unlink(temp_filename)
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')) 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",%sURI="%s"\n' % (group_name, audio_track.media_info['language_name'], audio_track.media_info['language'], 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')