Esempio n. 1
0
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
Esempio n. 2
0
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
Esempio n. 3
0
    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()
Esempio n. 4
0
    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()
Esempio n. 5
0
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
Esempio n. 6
0
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
Esempio n. 7
0
    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)
Esempio n. 8
0
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",
        "-",
    ]
Esempio n. 9
0
    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)
Esempio n. 10
0
    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)
Esempio n. 11
0
 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()
Esempio n. 12
0
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', '-']
Esempio n. 13
0
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)
Esempio n. 14
0
    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()