def convert_file(filename, codec, force): if force: add_y = " -y " else: add_y = "" cmd = "ffmpeg {add_y} -i {filename} -vcodec copy -acodec copy".format( **locals()) # copy vp9 stream to vfi container -> nice to parse if codec == 'vp9': conv_filename = "".join([filename, "_tmp.ivf"]) cmd += " {conv_filename}".format(**locals()) # create file with raw h264 stream formatted according to annex-b elif codec == 'h264': conv_filename = "".join([filename, "_tmp.h264"]) cmd += " -bsf:v h264_mp4toannexb {conv_filename}".format(**locals()) # create file with raw h265 stream formatted according to annex-b else: conv_filename = "".join([filename, "_tmp.h265"]) cmd += " -bsf:v hevc_mp4toannexb {conv_filename}".format(**locals()) # if there is already a temporary file, use this if -f is not specified if os.path.isfile(conv_filename) and not force: return conv_filename cmd_utils.run_command( cmd, "converting {filename} to {conv_filename}".format(**locals())) return conv_filename
def get_src_info(src): """ Get info about the SRC, as shown by ffprobe. Possible return keys, including example output: - codec_name: "h264" - codec_long_name: "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10" - profile: "High 4:4:4 Predictive" - codec_type: "video" - codec_time_base: "1/120" - codec_tag_string: "avc1" - codec_tag: "0x31637661" - width: 3840 - height: 2160 - coded_width: 3840 - coded_height: 2160 - has_b_frames: 0 - sample_aspect_ratio: "1:1" - display_aspect_ratio: "16:9" - pix_fmt: "yuv444p" - level: 52 - chroma_location: "left" - refs: 1 - is_avc: "true" - nal_length_size: "4" - r_frame_rate: "60/1" - avg_frame_rate: "60/1" - time_base: "1/15360" - start_pts: 0 - start_time: "0.000000" - duration_ts: 153600 - duration: "10.000000" - bit_rate: "1569904" - bits_per_raw_sample: "8" - nb_frames: "600" """ input_file = src.file_path cmd = "ffprobe -loglevel error -select_streams v -show_streams -of json '" + input_file + "'" if not os.path.isfile(src.info_path): stdout, _ = cmd_utils.run_command(cmd, name="get SRC info for " + str(src)) info = json.loads(stdout) returndata = info["streams"][0] videosize = get_stream_size(src) audiosize = get_stream_size(src, 'audio') info_to_dump = {} info_to_dump['md5sum'] = '-' info_to_dump['get_stream_size'] = {"v": videosize, "a": audiosize} info_to_dump['get_src_info'] = returndata with open(src.info_path, 'w') as outfile: yaml.dump(info_to_dump, outfile, default_flow_style=False) else: with open(src.info_path) as f_in: ydata = yaml.load(f_in, Loader=yaml.FullLoader) returndata = ydata['get_src_info'] return returndata
def get_processing_chain_version(): processing_chain_dir = get_processing_chain_dir() git_version, _ = cmd_utils.run_command('cd "' + processing_chain_dir + '" && git describe --always') with open(os.path.join(get_processing_chain_dir(), 'VERSION'), 'r') as version_f: major_version = version_f.readlines()[0].strip() version = git_version.strip() + " v" + major_version return version
def get_stream_size(segment, stream_type="video"): """ Return the video stream size in Bytes, as determined by summing up the individual frame size. stream_type: either "video" or "audio" """ switch = "v" if stream_type == "video" else "a" cmd = "ffprobe -loglevel error -select_streams " + switch + " -show_entries packet=size -of compact=p=0:nk=1 '" + segment.file_path + "'" if os.path.isfile(segment.file_path + '.yaml'): with open(segment.file_path + '.yaml') as f_in: ydata = yaml.load(f_in, Loader=yaml.FullLoader) size = ydata['get_stream_size'][switch] else: stdout, _ = cmd_utils.run_command( cmd, name="get accumulated frame size for " + str(segment)) size = sum([int(ll) for ll in stdout.split("\n") if ll != ""]) return size
def get_audio_frame_info(segment): """ Return a list of OrderedDicts with audio sample info, in presentation order Keys: - `segment`: basename of the segment file - `index`: index of the frame - `dts`: DTS of the sample - `size`: Size of the sample in bytes - `duration`: Duration of the sample in `s.msec` """ cmd = "ffprobe -loglevel error -select_streams a -show_packets -show_entries packet=duration_time,size,dts_time -of json '" + segment.file_path + "'" stdout, _ = cmd_utils.run_command(cmd, name="get AFI for " + str(segment)) info = json.loads(stdout)["packets"] ret = [] index = 0 for packet_info in info: ret.append( OrderedDict([('segment', segment.get_filename()), ('index', index), ('dts', float(packet_info['dts_time'])), ('size', int(packet_info['size'])), ('duration', float(packet_info['duration_time']))])) index += 1 return ret
def run(cli_args, test_config=None): if not test_config: test_config = cfg.TestConfig(cli_args.test_config, cli_args.filter_src, cli_args.filter_hrc, cli_args.filter_pvs) # get all pvs to be processed pvs_to_complete = [] for pvs_id, pvs in test_config.pvses.items(): if pvs.is_online() and cli_args.skip_online_services: continue pvs_to_complete.append(pvs) # aggregate/decode in parallel logger.info("will aggregate " + str(len(pvs_to_complete)) + " PVSes") cmd_list = [] # concatenate segments if the test type is "long" if test_config.is_long(): cmd_runner_concat = cmd_utils.ParallelRunner(cli_args.parallelism) pvs_commands = {} for pvs in pvs_to_complete: pvs_commands[pvs.pvs_id] = [] # decode segments cmd_runner_segments = cmd_utils.ParallelRunner( cli_args.parallelism) segment_iter = 0 for seg in pvs.segments: cmd = ffmpeg.create_avpvs_segment( seg, pvs, overwrite=cli_args.force, scale_avpvs_tosource=cli_args.avpvs_src_fps) cmd_name = "create AVPVS segment nr: " + str( segment_iter) + " for " + str(pvs) cmd_runner_segments.add_cmd(cmd, name=str(cmd_name)) segment_iter += 1 pvs_commands[pvs.pvs_id].append( cmd_runner_segments.return_command_list()) # concatenate segments cmd_concat = ffmpeg.create_avpvs_long_concat( pvs, overwrite=cli_args.force, scale_avpvs_tosource=cli_args.avpvs_src_fps) cmd_concat_name = "create AVPVS long for " + str(pvs) pvs_commands[pvs.pvs_id].append(cmd_concat) # add audio cmd_audio = ffmpeg.audio_mux(pvs, overwrite=cli_args.force) cmd_audio_name = "Muxing audio and video for " + str(pvs) pvs_commands[pvs.pvs_id].append(cmd_audio) # run or log all commands logger.debug(cmd_concat) logger.debug(cmd_audio) if cli_args.dry_run: cmd_runner_segments.log_commands() else: cmd_runner_segments.run_commands() cmd_utils.run_command(cmd_concat, name=str(cmd_concat_name)) cmd_utils.run_command(cmd_audio, name=str(cmd_audio_name)) # delete avpvs segments logger.info("Removing " + str(len(pvs.segments)) + " avpvs segments") if not cli_args.dry_run: os.remove(pvs.get_avpvs_file_list()) os.remove(pvs.get_tmp_wo_audio_path()) for seg in pvs.segments: os.remove(seg.get_tmp_path()) # add stalling if needed pvs_with_buffering = [ pvs for pvs in pvs_to_complete if pvs.has_buffering() ] cmd_runner_add_buffer = cmd_utils.ParallelRunner(cli_args.parallelism) if len(pvs_with_buffering): logger.info("will add stalling to " + str(len(pvs_with_buffering)) + " PVSes") for pvs in pvs_with_buffering: input_file = pvs.get_avpvs_wo_buffer_file_path() output_file = pvs.get_avpvs_file_path() bufferstring = str(pvs.get_buff_events_media_time()).replace( ' ', '') pix_fmt = pvs.get_pix_fmt_for_avpvs() if cli_args.force: overwrite_spec = "-f" else: overwrite_spec = "" cmd = 'bufferer -i {input_file} -o {output_file} -b {bufferstring} --force-framerate --black-frame' \ '-v ffv1 -a pcm_s16le -x {pix_fmt} -s {cli_args.spinner_path} {overwrite_spec}'.format(**locals()) cmd_name = str(pvs) + ' buffering' cmd_runner_add_buffer.add_cmd(cmd, name=str(cmd_name)) pvs_commands[pvs.pvs_id].append(cmd) if cli_args.dry_run: cmd_runner_add_buffer.log_commands() sys.exit(0) else: for pvs in pvs_to_complete: write_to_p03_logfile(pvs, pvs_commands[pvs.pvs_id]) cmd_runner_add_buffer.run_commands() if cli_args.remove_intermediate: logger.info("removing " + str(len(pvs_with_buffering)) + " intermediate video files") for pvs_name in pvs_with_buffering: os.remove(pvs.get_avpvs_wo_buffer_file_path()) # Only run decoding if the test type is "short", only one segment assumed else: cmd_runner = cmd_utils.ParallelRunner(cli_args.parallelism) for pvs in pvs_to_complete: if cli_args.skip_online_services and pvs.is_online(): logger.warn( "Skipping PVS {} because it is an online PVS".format(pvs)) continue current_command = ffmpeg.create_avpvs_short( pvs, overwrite=cli_args.force, scale_avpvs_tosource=cli_args.avpvs_src_fps) cmd_runner.add_cmd(current_command, name="Create AVPVS short for " + str(pvs)) if not cli_args.dry_run and current_command is not None: write_to_p03_logfile(pvs, [ current_command, ]) if cli_args.dry_run: cmd_runner.log_commands() return test_config cmd_runner.run_commands() return test_config
def get_video_frame_info(segment, info_type="packet"): """ Return a list of OrderedDicts with video frame info, in decoding or presentation order info_type: "packet" or "frame", if packet: decoding order, if frame: presentation order Return keys: - `segment`: basename of the segment file - `index`: index of the frame - `frame_type`: `I` or `Non-I` (for decoding order) or `I`, `P`, `B` (for presentation order) - `dts`: DTS of the frame (only for decoding order) - `pts`: PTS of the frame - `size`: Size of the packet in bytes (including SPS, PPS for first frame, and AUD units for subsequent frames) - `duration`: Duration of the frame in `s.msec` """ if info_type == "packet": cmd = "ffprobe -loglevel error -select_streams v -show_packets -show_entries packet=pts_time,dts_time,duration_time,size,flags -of json '" + segment.file_path + "'" elif info_type == "frame": cmd = "ffprobe -loglevel error -select_streams v -show_frames -show_entries frame=pkt_pts_time,pkt_dts_time,pkt_duration_time,pkt_size,pict_type -of json '" + segment.file_path + "'" else: logger.error("wrong info type, can be 'packet' or 'frame'") sys.exit(1) stdout, _ = cmd_utils.run_command(cmd, name="get VFI for " + str(segment)) info = json.loads(stdout)[info_type + "s"] # Assemble info into OrderedDict if info_type == "packet": ret = [] index = 0 default_duration = next( (x["duration_time"] for x in info if "duration_time" in x.keys()), "NaN") for packet_info in info: frame_type = "I" if packet_info['flags'] == "K_" else "Non-I" if 'dts_time' in packet_info.keys(): dts = float(packet_info['dts_time']) else: dts = "NaN" if 'duration_time' in packet_info.keys(): duration = float(packet_info['duration_time']) else: duration = default_duration ret.append( OrderedDict([('segment', segment.get_filename()), ('index', index), ('frame_type', frame_type), ('dts', dts), ('size', packet_info['size']), ('duration', duration)])) index += 1 elif info_type == "frame": ret = [] index = 0 for frame_info in info: if 'pts_time' in frame_info.keys(): pts = float(frame_info['pts_time']) else: pts = "NaN" ret.append( OrderedDict([ ('segment', segment.get_filename()), ('index', index), ('frame_type', frame_info['pict_type']), ('pts', pts), ('size', int(frame_info['pkt_size'])), ('duration', float(frame_info['pkt_duration_time'])) ])) index += 1 else: # cannot happen pass # fix for missing duration in VP9: estimate duration from DTS difference ret = fix_durations(ret) return ret
def get_segment_info(segment): """ Get the info about the segment, as shown by ffprobe, for use in .qchanges file Returns an OrderedDict, with the keys: - `segment_filename`: Basename of the segment file - `file_size`: Size of the file in bytes - `video_duration`: Duration of the video in `s.msec` - `video_frame_rate`: Framerate in Hz - `video_bitrate`: Bitrate of the video stream in kBit/s - `video_target_bitrate`: Target bitrate of the video stream in kBit/s (may be empty/unknown) - `video_width`: Width in pixels - `video_height`: Height in pixels - `video_codec`: Video codec (`h264`, `hevc`, `vp9`) - `video_profile`: Video profile - `audio_duration`: Duration of the audio in `s.msec` - `audio_sample_rate`: Audio sample rate in Hz - `audio_codec`: Audio codec name (`aac`) - `audio_bitrate`: Bitrate of the video stream in kBit/s """ input_file = segment.file_path if sys.platform == "darwin": cmd = "stat -f '%z' '" + input_file + "'" else: cmd = "stat -c '%s' '" + input_file + "'" stdout, _ = cmd_utils.run_command(cmd, name="get segment size for " + str(segment)) segment_size = int(stdout.strip()) cmd = "ffprobe -loglevel error -show_streams -of json '" + input_file + "'" stdout, _ = cmd_utils.run_command(cmd, name="get segment video info for " + str(segment)) info = json.loads(stdout) has_video = False has_audio = False for stream_info in info["streams"]: if stream_info["codec_type"] == "video": video_info = stream_info has_video = True elif stream_info["codec_type"] == "audio": audio_info = stream_info has_audio = True if not has_video: logger.error("No video stream found in segment " + str(segment)) sys.exit(1) if 'duration' in video_info.keys(): video_duration = float(video_info['duration']) elif 'tags' in video_info.keys() and 'DURATION' in video_info['tags']: duration_str = video_info['tags']['DURATION'] hms, msec = duration_str.split('.') total_dur = sum( int(x) * 60**i for i, x in enumerate(reversed(hms.split(":")))) video_duration = total_dur + float("0." + msec) else: info_type = 'packet' cmd = "ffprobe -loglevel error -select_streams v -show_packets -show_entries packet=pts_time,dts_time,duration_time,size,flags -of json '" + segment.file_path + "'" stdout, _ = cmd_utils.run_command(cmd, name="get VFI for " + str(segment)) info = json.loads(stdout)[info_type + "s"] index = -1 while True: packet_info = info[index] if 'dts_time' in packet_info.keys( ) and 'duration_time' in packet_info.keys(): video_duration = float( packet_info['dts_time']) + abs(index) * float( packet_info['duration_time']) break index = index - 1 logger.warning("Calculated duration of segment " + str(segment) + " manually. Might not be perfectly accurate.") if not video_duration: logger.error( "Video duration of " + str(segment) + " was calculated as zero! Make sure that the input file is correct." ) sys.exit(1) if 'bit_rate' in video_info.keys(): video_bitrate = round(float(video_info['bit_rate']) / 1024.0, 2) else: # fall back to calculating from accumulated frame duration stream_size = get_stream_size(segment) video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2) if hasattr(segment, "quality_level"): video_target_bitrate = segment.quality_level.video_bitrate else: video_target_bitrate = 0 # override designation of video profile: if 'profile' in video_info.keys(): video_profile = fix_video_profile_string(video_info['profile']) else: video_profile = "" ret = OrderedDict([('segment_filename', segment.filename), ('file_size', segment_size), ('video_duration', video_duration), ('video_frame_rate', float(Fraction(video_info['r_frame_rate']))), ('video_bitrate', video_bitrate), ('video_target_bitrate', video_target_bitrate), ('video_width', video_info['width']), ('video_height', video_info['height']), ('video_codec', video_info['codec_name']), ('video_profile', video_profile)]) if has_audio: if 'duration' in audio_info.keys(): audio_duration = float(audio_info['duration']) elif 'tags' in audio_info.keys() and 'DURATION' in audio_info['tags']: duration_str = audio_info['tags']['DURATION'] hms, msec = duration_str.split('.') total_dur = sum( int(x) * 60**i for i, x in enumerate(reversed(hms.split(":")))) audio_duration = total_dur + float("0." + msec) elif 'nb_frames' in audio_info.keys(): audio_duration = float(audio_info['nb_frames']) / float( audio_info['sample_rate']) else: logger.error("Could not extract audio duration from " + str(segment)) sys.exit(1) if 'bit_rate' in audio_info.keys(): audio_bitrate = round(float(audio_info['bit_rate']) / 1024.0, 2) else: # fall back to calculating from accumulated frame duration stream_size = get_stream_size(segment, stream_type="audio") audio_bitrate = round((stream_size * 8 / 1024.0) / audio_duration, 2) ret.update( OrderedDict([('audio_duration', audio_duration), ('audio_sample_rate', audio_info['sample_rate']), ('audio_codec', audio_info['codec_name']), ('audio_bitrate', audio_bitrate)])) return ret
def generate_full_segment(self, filename, codec, ten_bit=False, audio=False): if ten_bit: ffmpeg_version = "ffmpeg10" else: ffmpeg_version = "ffmpeg" root, ext = os.path.splitext(filename) full_video_path = os.path.join(self.video_segments_folder, filename) dload_path = os.path.join(self.video_segments_folder, root) video_outfile = os.path.join(dload_path, root + "_video_only" + ext) concat_cmd = ffmpeg_version + " -y -i " + video_outfile + " -strict -2 -c copy " + full_video_path video_init_element = None video_parts = [] video_init_found = False directory = os.fsencode(dload_path) for video_part in os.listdir(directory): video_part_name = os.fsdecode(video_part) if video_part_name.endswith("init.hdr") and codec == "vp9" \ or video_part_name.endswith("init.mp4") and codec in ["h264", "h265", "hevc"]: if video_init_found: logger.warning("Second init file found. Please clean your download folder " + dload_path) video_init_element = video_part_name video_init_found = True continue elif video_part_name.endswith(".chk") and codec == "vp9" \ or video_part_name.endswith(".m4s") and codec in ["h264", "h265", "hevc"]: raw_name = os.path.splitext(video_part_name)[0] part_number = int(raw_name.split("_")[-1]) while len(video_parts) <= part_number: video_parts.append("Dummy_entry") video_parts[part_number] = video_part_name continue if not video_init_found: logger.error("No init file found! Aborting") exit(-1) cmd = ffmpeg_version + """ -y -i \"concat:""" + os.path.join(dload_path, video_init_element) for video_part_name in video_parts: cmd = cmd + "|" + os.path.join(dload_path, video_part_name) cmd = cmd + """" -c copy -strict -2 """ + video_outfile cmd_utils.run_command(cmd) if audio: audio_init_element = None audio_parts = [] audio_init_found = False dload_path = os.path.join(dload_path, "audio") logger.debug(dload_path) directory = os.fsencode(dload_path) audio_outfile = os.path.join(dload_path, root + "_audio_only.mp4") for audio_part in os.listdir(directory): audio_part_name = os.fsdecode(audio_part) logger.debug(audio_part_name) if audio_part_name.endswith("init.hdr") and codec == "vp9" \ or audio_part_name.endswith("init.mp4") and codec in ["h264", "h265", "hevc"]: if audio_init_found: logger.warning("Second init file found. Please clean your download folder " + dload_path) audio_init_element = audio_part_name audio_init_found = True continue elif audio_part_name.endswith(".chk") and codec == "vp9" \ or audio_part_name.endswith(".m4s") and codec in ["h264", "h265", "hevc"]: raw_name = os.path.splitext(audio_part_name)[0] part_number = int(raw_name.split("_")[-1]) while len(audio_parts) <= part_number: audio_parts.append("Dummy_entry") audio_parts[part_number] = audio_part_name # audio_parts.append(audio_part_name) continue if audio_init_found: # TODO: implement force overwriting cmd = ffmpeg_version + """ -y -i \"concat:""" + os.path.join(dload_path, audio_init_element) for audio_part_name in audio_parts: cmd = cmd + "|" + os.path.join(dload_path, audio_part_name) cmd = cmd + """" -c copy -strict -2 """ + audio_outfile cmd_utils.run_command(cmd) concat_cmd = ffmpeg_version + " -y -i " + video_outfile + " -i " + audio_outfile + " -strict -2 -c copy " + full_video_path else: logger.warning("No audio file for " + root + " found. Will create a video without audio!") cmd_utils.run_command(concat_cmd)