def set_target_quality(project): """ Av1an setup for target_quality :param project: the Project """ if project.vmaf_path: if not Path(project.vmaf_path).exists(): print(f"No model with this path: {Path(project.vmaf_path).as_posix()}") terminate() if project.probes < 4: print('Target quality with less than 4 probes is experimental and not recommended') terminate() encoder = ENCODERS[project.encoder] if project.encoder not in ('x265', 'svt_av1') and project.target_quality_method == 'per_frame': print(f":: Per frame Target Quality is not supported for selected encoder\n:: Supported encoders: x265, svt_av1") exit() # setting range for q values if project.min_q is None: project.min_q, _ = encoder.default_q_range assert project.min_q > 1 if project.max_q is None: _, project.max_q = encoder.default_q_range
def create_video_queue_segment(project: Project, split_locations: List[int]) -> List[Chunk]: """ Create a list of chunks using segmented files :param project: Project :param split_locations: a list of frames to split on :return: A list of chunks """ # segment into separate files segment(project.input, project.temp, split_locations) # get the names of all the split files source_path = project.temp / 'split' queue_files = [x for x in source_path.iterdir() if x.suffix == '.mkv'] queue_files.sort(key=lambda p: p.stem) if len(queue_files) == 0: er = 'Error: No files found in temp/split, probably splitting not working' print(er) log(er) terminate() chunk_queue = [ create_chunk_from_segment(project, index, file) for index, file in enumerate(queue_files) ] return chunk_queue
def check_exes(self): """ Checking required executables """ if not find_executable("ffmpeg"): print("No ffmpeg") terminate() if self.chunk_method in ["vs_ffms2", "vs_lsmash"]: if not find_executable("vspipe"): print("vspipe executable not found") terminate() try: import vapoursynth plugins = vapoursynth.get_core().get_plugins() if ( self.chunk_method == "vs_lsmash" and "systems.innocent.lsmas" not in plugins ): print("lsmas is not installed") terminate() if ( self.chunk_method == "vs_ffms2" and "com.vapoursynth.ffms2" not in plugins ): print("ffms2 is not installed") terminate() except ModuleNotFoundError: print("Vapoursynth is not installed") terminate()
def check_exes(self): """ Checking required executables """ if not find_executable('ffmpeg'): print('No ffmpeg') terminate() if self.chunk_method in ['vs_ffms2', 'vs_lsmash']: if not find_executable('vspipe'): print('vspipe executable not found') terminate() try: import vapoursynth plugins = vapoursynth.get_core().get_plugins() if self.chunk_method == 'vs_lsmash' and "systems.innocent.lsmas" not in plugins: print('lsmas is not installed') terminate() if self.chunk_method == 'vs_ffms2' and "com.vapoursynth.ffms2" not in plugins: print('ffms2 is not installed') terminate() except ModuleNotFoundError: print('Vapoursynth is not installed') terminate()
def find_aom_keyframes(stat_file, key_freq_min): keyframes_list = [] number_of_frames = round(os.stat(stat_file).st_size / 208) - 1 dict_list = [] try: with open(stat_file, 'rb') as file: frame_buf = file.read(208) while len(frame_buf) > 0: stats = struct.unpack('d' * 26, frame_buf) p = dict(zip(fields, stats)) dict_list.append(p) frame_buf = file.read(208) except Exception as e: print('Get exception:', e) print('Recomended to switch to different method of scenedetection') terminate() # intentionally skipping 0th frame and last 16 frames frame_count_so_far = 1 for i in range(1, number_of_frames - 16): is_keyframe = False # https://aomedia.googlesource.com/aom/+/ce97de2724d7ffdfdbe986a14d49366936187298/av1/encoder/pass2_strategy.c#2065 if frame_count_so_far >= key_freq_min: is_keyframe = test_candidate_kf(dict_list, i, frame_count_so_far) if is_keyframe: keyframes_list.append(i) frame_count_so_far = 0 frame_count_so_far += 1 return keyframes_list
def aom_keyframes(video_path: Path, stat_file, min_scene_len, ffmpeg_pipe, video_params, is_vs, quiet): """[Get frame numbers for splits from aomenc 1 pass stat file] """ log(f'Started aom_keyframes scenedetection\nParams: {video_params}\n') total = frame_probe_fast(video_path, is_vs) f, e = compose_aomsplit_first_pass_command(video_path, stat_file, ffmpeg_pipe, video_params, is_vs) tqdm_bar = None if (not quiet) and (not (tqdm is None)): tqdm_bar = tqdm(total=total, initial=0, dynamic_ncols=True, unit="fr", leave=True, smoothing=0.2) ffmpeg_pipe = subprocess.Popen(f, stdout=PIPE, stderr=STDOUT) pipe = subprocess.Popen(e, stdin=ffmpeg_pipe.stdout, stdout=PIPE, stderr=STDOUT, universal_newlines=True) encoder_history = deque(maxlen=20) frame = 0 while True: line = pipe.stdout.readline() if len(line) == 0 and pipe.poll() is not None: break line = line.strip() if line: encoder_history.append(line) if quiet or (tqdm is None): continue match = re.search(r"frame.*?/([^ ]+?) ", line) if match: new = int(match.group(1)) if new > frame: tqdm_bar.update(new - frame) frame = new if pipe.returncode != 0 and pipe.returncode != -2: # -2 is Ctrl+C for aom enc_hist = '\n'.join(encoder_history) er = f"\nAom first pass encountered an error: {pipe.returncode}\n{enc_hist}" log(er) print(er) if not stat_file.exists(): terminate() else: # aom crashed, but created keyframes.log, so we will try to continue print("WARNING: Aom first pass crashed, but created a first pass file. Keyframe splitting may not be accurate.") # aom kf-min-dist defaults to 0, but hardcoded to 3 in pass2_strategy.c test_candidate_kf. 0 matches default aom behavior # https://aomedia.googlesource.com/aom/+/8ac928be918de0d502b7b492708d57ad4d817676/av1/av1_cx_iface.c#2816 # https://aomedia.googlesource.com/aom/+/ce97de2724d7ffdfdbe986a14d49366936187298/av1/encoder/pass2_strategy.c#1907 min_scene_len = 0 if min_scene_len is None else min_scene_len keyframes = find_aom_keyframes(stat_file, min_scene_len) return keyframes
def match_line(self, line: str): """Extract number of encoded frames from line. :param line: one line of text output from the encoder :return: match object from re.search matching the number of encoded frames""" if "error" in line.lower(): print("\n\nERROR IN ENCODING PROCESS\n\n", line) terminate() return re.search(r"Encoding frame\s+(\d+)", line)
def startup_check(project: Project): """ Performing essential checks at startup_check Set constant values """ if sys.version_info < (3, 6): print("Python 3.6+ required") sys.exit() if sys.platform == "linux": def restore_term(): os.system("stty sane") atexit.register(restore_term) if not project.chunk_method: project.select_best_chunking_method() project.is_vs = is_vapoursynth(project.input[0]) if project.is_vs: project.chunk_method = "vs_ffms2" project.check_exes() set_target_quality(project) if (project.reuse_first_pass and project.encoder != "aom" and project.split_method != "aom_keyframes"): print("Reusing the first pass is only supported with \ the aom encoder and aom_keyframes split method.") terminate() setup_encoder(project) # No check because vvc if project.encoder == "vvc": project.no_check = True if project.encoder == "svt_vp9" and project.passes == 2: print( "Implicitly changing 2 pass svt-vp9 to 1 pass\n2 pass svt-vp9 isn't supported" ) project.passes = 1 project.audio_params = shlex.split(project.audio_params) project.ffmpeg = shlex.split(project.ffmpeg) project.pix_format = ["-strict", "-1", "-pix_fmt", project.pix_format] project.ffmpeg_pipe = [ *project.ffmpeg, *project.pix_format, "-f", "yuv4mpegpipe", "-", ]
def match_line(self, line: str): """Extract number of encoded frames from line. :param line: one line of text output from the encoder :return: match object from re.search matching the number of encoded frames""" if 'error' in line.lower(): print('\n\nERROR IN ENCODING PROCESS\n\n', line) terminate() return re.search(r"encoded.*? ([^ ]+?) ", line)
def match_line(self, line: str): """Extract number of encoded frames from line. :param line: one line of text output from the encoder :return: match object from re.search matching the number of encoded frames""" if "fatal" in line.lower(): print("\n\nERROR IN ENCODING PROCESS\n\n", line) terminate() if "Pass 2/2" in line or "Pass 1/1" in line: return re.search(r"frame.*?/([^ ]+?) ", line)
def encoding_loop(self): with concurrent.futures.ThreadPoolExecutor(max_workers=self.project.workers) as executor: future_cmd = {executor.submit(self.encode_chunk, cmd): cmd for cmd in self.chunk_queue} for future in concurrent.futures.as_completed(future_cmd): try: future.result() except Exception as exc: _, _, exc_tb = sys.exc_info() print(f'Encoding error {exc}\nAt line {exc_tb.tb_lineno}') terminate() self.project.counter.close()
def startup_check(project: Project): """ Performing essential checks at startup_check Set constant values """ if sys.version_info < (3, 6): print('Python 3.6+ required') sys.exit() if sys.platform == 'linux': def restore_term(): os.system("stty sane") atexit.register(restore_term) if not project.chunk_method: project.select_best_chunking_method() # project.is_vs = is_vapoursynth(project.input) if project.is_vs: project.chunk_method = 'vs_ffms2' project.check_exes() set_target_quality(project) if project.reuse_first_pass and project.encoder != 'aom' and project.split_method != 'aom_keyframes': print('Reusing the first pass is only supported with \ the aom encoder and aom_keyframes split method.') terminate() setup_encoder(project) # No check because vvc if project.encoder == 'vvc': project.no_check = True if project.encoder == 'svt_vp9' and project.passes == 2: print("Implicitly changing 2 pass svt-vp9 to 1 pass\n2 pass svt-vp9 isn't supported") project.passes = 1 project.audio_params = shlex.split(project.audio_params) project.ffmpeg = shlex.split(project.ffmpeg) project.pix_format = ['-strict', '-1', '-pix_fmt', project.pix_format] project.ffmpeg_pipe = [*project.ffmpeg, *project.pix_format,'-color_range', '0', '-f', 'yuv4mpegpipe', '-']
def setup_encoder(project: Project): """ Setup encoder params and passes :param project: the Project """ encoder = ENCODERS[project.encoder] # validate encoder settings settings_valid, error_msg = encoder.is_valid(project) if not settings_valid: print(error_msg) terminate() if project.passes is None: project.passes = encoder.default_passes project.video_params = encoder.default_args if project.video_params is None \ else shlex.split(project.video_params) validate_inputs(project)
def concat_routine(self): """ Runs the concatenation routine with project :param project: the Project :return: None """ try: if self.encoder == "vvc": vvc_concat(self.temp, self.output_file.with_suffix(".h266")) elif self.mkvmerge: concatenate_mkvmerge(self.temp, self.output_file) else: concatenate_ffmpeg(self.temp, self.output_file, self.encoder) except Exception as e: _, _, exc_tb = sys.exc_info() print( f"Concatenation failed, error At line: {exc_tb.tb_lineno}\nError:{str(e)}" ) log(f"Concatenation failed, aborting, error: {e}") terminate()