JustPaste.it

f'This script requires python version 3.6 or higher.'
from common.cli import Cli
from common.filectrl import FileCtrl
from common.processctrl import Logger, call_sanity_check, call, MultiProcess
from common.misc import decorate, check_extention, time_msg, get_scale_metadata_setting
import common.dv_workflow as dv
import common.app_cmd as cmd
from datetime import datetime
import sys
import os
import shutil
import math
import platform
import time
from common.globals import set_logger, set_file_ctrl, set_multiproc, fc, log, mp


def windows_sleep():
    if platform.system() == 'Windows':
        time.sleep(5)


def get_mdpp_help():
    print((
        'Post-processing configuration options: \n'
        '  none - disable post-processing step entirely. \n'
        '  L5   - enable post-processing for L5 metadata only. \n'
        '  full - enable standard post-processing. \n'
    ))


def validate(cli):
    sanity_check_apps = {'PREPROC': ('dee_dv5_preproc', '1.0'),
                         'VES_MUXER': ('dee_dv_vesmux', '1.1'),
                         'POSTPROC': ('dee_dv_postproc', '1.1'),
                         'PARSE_MEZZ': ('dee_dv_parse_mezz', '1.0'),
                         'STREAM_SPLITTER': ('dee_stream_splitter', '1.0'),
                         'MEDIAINFO': ('mediainfo', None)}

    for tag, (app, version) in sanity_check_apps.items():
        if call_sanity_check(tag, [os.path.join(cli.toolset_dir, app), '-h'], log().errors, version):
            raise RuntimeError

    if call_sanity_check('MP4_MUXER', [os.path.join(cli.toolset_dir, 'mp4muxer'), '-h',
                         '--log-file', fc().register_temp_file('mp4muxer_log_sanity_check')], log().errors):
        raise RuntimeError

    if call_sanity_check('FFMPEG', [cli.ffmpeg, '-version'], log().errors):
        raise RuntimeError
    if cli.dvesverifier:
        if call_sanity_check('DVESVERIFIER', [cli.dvesverifier], log().errors):
            raise RuntimeError

    license_check_apps = ['dee_dv5_preproc', 'dee_dv_vesmux',
                          'dee_dv_postproc', 'dee_dv_parse_mezz', 'dee_stream_splitter']
    for app in license_check_apps:
        if call('LICENSE_CHECK', [os.path.join(cli.toolset_dir, app), '-l', cli.license], 'all', log().errors):
            raise RuntimeError


def max_scene_frames(gop_size):
    max_frames = gop_size * 2
    if max_frames > 255:
        max_frames = min(gop_size, 255)
    return max_frames


def configure_dv_preproc(cli, cfg):
    pipe_buffer_size = dv.get_pipe_buffer_size(
        cli.yuv_buffer_size, cfg['target_resolution'])
    dv_preproc_cfg = {'license': cli.license,
                      'input-format': cli.input_format,
                      'resize-options': f"scale={cfg['target_resolution']}" if cfg['target_resolution'] != cfg['input_resolution'] else None,
                      'input': cli.input,
                      'input-metadata': cli.input_metadata,
                      'metadata-offset': cli.metadata_offset,
                      'start': cli.start,
                      'duration': cli.duration,
                      'overwrite': 1,
                      'decoder': dv.get_decoder(cli.input_format),
                      'max-scene-frames': max_scene_frames(cfg['gop_size']),
                      'show-frames': 'counters' if cli.progress else None,
                      'progress': '1' if cli.progress else '0',
                      'keep-temp': cli.keep_temp,
                      'temp-dir': fc().get_temp_dir(),
                      'loglevel': 'info:timestamps=0' if cli.print_all == 'all' or cli.progress else 'error:timestamps=0'}

    if cli.encoder_pass_num == 2 and cli.data_stream:
        cmds = []
        # pass 1
        dv_preproc_cfg.update({'output': dv.get_output_name(cfg['output'], True, pipe_buffer_size),
                               'output-rpu': "NULL"})
        cmds.append(cmd.DvPreproc(os.path.join(
            cli.toolset_dir, 'dee_dv5_preproc'), dv_preproc_cfg).get_cmd())
        # pass 2
        dv_preproc_cfg.update({'output': dv.get_output_name(cfg['output'], True, pipe_buffer_size),
                               'output-rpu': dv.get_output_name(cfg['output_rpu'], True, cfg['rpu_pipe_buffer_size']),
                               })
        cmds.append(cmd.DvPreproc(os.path.join(
            cli.toolset_dir, 'dee_dv5_preproc'), dv_preproc_cfg).get_cmd())
        return cmds
    else:
        dv_preproc_cfg.update({'output': dv.get_output_name(cfg['output'], cli.data_stream, pipe_buffer_size),
                               'output-rpu': dv.get_output_name(cfg['output_rpu'], cli.data_stream, cfg.get('rpu_pipe_buffer_size')),
                               })
        return [cmd.DvPreproc(os.path.join(cli.toolset_dir, 'dee_dv5_preproc'), dv_preproc_cfg).get_cmd()]


def configure_encoder(cli, cfg):
    data_rate = cfg['data_rate']
    enc_cfg = {'ffmpeg': cli.ffmpeg,
               'input_resolution': cfg['input_resolution'],
               'target_resolution': cfg['target_resolution'].replace('x', ':'),
               'frame_rate': cfg['frame_rate'],
               'loglevel': 'info' if cli.print_all == 'all' else 'error',
               'input': dv.get_input_name(cfg['input'], cli.data_stream, None),
               'preset': cli.preset,
               'data_rate': data_rate,
               'gop_size': cfg['gop_size'],
               'lookahead': min(cfg['gop_size'], 240),
               'buffer_size': 160000 * cli.gop_duration,
               'pass_str': '',
               'output': dv.get_output_name(cfg['output'], cli.data_stream, None),
               }
    encoder = ('{ffmpeg} -y '
               '-f rawvideo '
               '-s {input_resolution} '
               '-pix_fmt yuv420p10le '
               '-loglevel {loglevel} '
               '-framerate {frame_rate} '
               '-i {input} '
               '-vf scale=3840:2160'  
               '-c:v libx265 -preset {preset} ' # changing this would over ride dovi script settings
               '-x265-params crf=17:deblock=-2-2:aq-mode=2:min-keyint=23:keyint=250:level-idc=5.1:no-open-gop=1:aud=1:hrd=1:repeat-headers=1:sar=1:sao=0:chromaloc=0:colormatrix=2:colorprim=2:transfer=2:' 
               'vbv-maxrate=160000:vbv-bufsize=160000:'
               'input-csp=i420:fps={frame_rate}:range=full:' # must be full range
               'no-info=0:log-level=0'
               '{pass_str} '
               '-f hevc {output} '
               '-nostdin')

    if cli.encoder_pass_num == 1:
        return [encoder.format(**enc_cfg).split(' ')]
    elif cli.encoder_pass_num == 2:
        enc_cmds = []
        enc_cfg_pass_2 = dict(enc_cfg)
        prefix = os.path.join(fc().get_temp_dir(), cfg['pass_log'])
        pass_str = ":pass={num}:stats='{prefix}'"

        # pass 1
        enc_cfg['pass_str'] = pass_str.format(num=1, prefix=prefix)
        enc_cfg['output'] = 'NUL' if platform.system(
        ) == 'Windows' else '/dev/null'
        enc_cmds.append(encoder.format(**enc_cfg).split(' '))

        # pass 2
        enc_cfg_pass_2['pass_str'] = pass_str.format(num=2, prefix=prefix)
        enc_cmds.append(encoder.format(**enc_cfg_pass_2).split(' '))
        return enc_cmds


def configure_dv_vesmux(cli, cfg):
    bl_pipe_buffer = 4096
    dv_vesmux_cfg = {'license': cli.license,
                     'overwrite': 1,
                     'input-bl': dv.get_input_name(cfg['input_bl'], cli.data_stream, bl_pipe_buffer),
                     'input-rpu': dv.get_input_name(cfg['input_rpu'], cli.data_stream, cfg.get('rpu_pipe_buffer_size'), 'true'),
                     'output': fc().register_temp_file(cfg['output']),
                     'loglevel': 'info:timestamps=0' if cli.print_all == 'all' or cli.progress else 'error:timestamps=0'}
    return cmd.DvVesmux(os.path.join(cli.toolset_dir, 'dee_dv_vesmux'), dv_vesmux_cfg).get_cmd()


def configure_dv_postproc(cli, cfg):
    if cli.mdpp == 'none':
        return None

    dv_postproc_cfg = {'license': cli.license,
                       'overwrite': 1,
                       'progress': cli.progress,
                       'input': fc().get_temp_file(cfg['input']),
                       'scale-metadata': get_scale_metadata_setting(cfg['input_resolution'], cfg['target_resolution']),
                       'dv-profile': 5,
                       'output': fc().register_temp_file(cfg['output']),
                       'loglevel': 'info:timestamps=0' if cli.print_all == 'all' or cli.progress else 'error:timestamps=0'}
    if cli.mdpp == 'L5':
        dv_postproc_cfg.update({'update-L4': 0,
                                'update-L6': 0,
                                'L1-filtering': 'none', })

    return cmd.DvPostproc(os.path.join(cli.toolset_dir, 'dee_dv_postproc'), dv_postproc_cfg).get_cmd()


def create_output_file(cli, cfg):
    output = fc().register_file(cfg['output_path'])
    if output.endswith('.mp4'):
        mp4muxer = [os.path.join(cli.toolset_dir, 'mp4muxer'),
                    '--overwrite',
                    '--input-file', fc().get_temp_file(cfg['input_path']),
                    '--dv-profile', '5',
                    '--log-file', fc().register_temp_file(cfg['log_path']),
                    '--mpeg4-comp-brand', 'dby1',
                    '--output-file', output]
        if cli.codec_id == 'dvh1':
            mp4muxer.append('--dvh1flag')
        call(cfg['id'], mp4muxer, cli.print_all, log().errors,
             log().infos, None, decorate, 'MP4_MUXER')
    else:
        shutil.move(fc().get_temp_file(cfg['input_path']), output)


def configure_dv_es_verifier(cli, cfg):
    return[cli.dvesverifier,
           '-i', fc().get_temp_file(cfg['input']),
           '-dp', '5']


def configure_splitter(cli, cfg):
    splitter_cfg = {'license': cli.license,
                    'block-size': cfg['block_size'] if 'block_size' in cfg else None,
                    'input': dv.get_input_name(cfg['input'], True, cfg['input_buffer'] if 'input_buffer' in cfg else None, 'true') + ':read_threshold=1'}
    splitter_cfg = cmd.StreamSplitter(os.path.join(
        cli.toolset_dir, 'dee_stream_splitter'), splitter_cfg).get_cmd()

    # Every ouput path needs to be a separate element in list, otherwise they are wrapped up in quotes and interpreted as one path
    return splitter_cfg + ['--output'] + [dv.get_output_name(o, True, cfg['output_buffer'] if 'output_buffer' in cfg else None, '0') for o in cfg['output']]


def make_prefix(file_no, target_resolution):
    return str(file_no) + '_' + target_resolution + '_'


def data_stream_workflow(cli, cfg):
    rpu_pipe_buffer_size = 4096 * cfg['gop_size']
    original_resolution = cfg['input_resolution']

    for i, (target_resolution, output_path, data_rate) in enumerate(zip(cli.target_resolution, cli.output, cli.data_rate)):
        prefix = make_prefix(i, target_resolution)
        preproc = configure_dv_preproc(cli, {'input_resolution': original_resolution,
                                             'target_resolution': target_resolution,
                                             'gop_size': cfg['gop_size'],
                                             'output_rpu': prefix + 'dv_preproc_output_rpu.bin',
                                             'rpu_pipe_buffer_size': rpu_pipe_buffer_size,
                                             'output': prefix + 'dv_preproc_output.yuv'})  # list of cmds

        encoder = configure_encoder(cli, {'input': prefix + 'dv_preproc_output.yuv',
                                          'gop_size': cfg['gop_size'],
                                          'data_rate': data_rate,
                                          'input_resolution': target_resolution,
                                          'target_resolution': target_resolution,
                                          'frame_rate': cfg['frame_rate'],
                                          'pass_log': prefix + 'ffmpegPassLog',
                                          'output': prefix + 'ffmpeg_output_hevc.hevc'})  # list of cmds

        vesmux = configure_dv_vesmux(cli, {'input_bl': prefix + 'ffmpeg_output_hevc.hevc',
                                           'input_rpu': prefix + 'dv_preproc_output_rpu.bin',
                                           'rpu_pipe_buffer_size': rpu_pipe_buffer_size,
                                           'output': prefix + 'dv_vesmux_output.hevc'})

        postproc = configure_dv_postproc(cli, {'input': prefix + 'dv_vesmux_output.hevc',
                                               'input_resolution': target_resolution,
                                               'target_resolution': target_resolution,
                                               'output': prefix + 'dv_postproc_output_hevc.hevc'})
        if cli.encoder_pass_num == 1:
            t1 = datetime.now()
            mp().run_multiproc(call, f"[{target_resolution}] PREPROC", preproc[0], "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'PREPROC')
            windows_sleep()
            mp().run_multiproc(call, f"[{target_resolution}] VES_MUXER", vesmux, cli.print_all, log().errors, None, mp().lock, decorate, 'VES_MUXER')
            windows_sleep()
            mp().run_multiproc(call, f"[{target_resolution}] ENCODER", encoder[0], cli.print_all, log().errors, None, mp().lock, decorate, 'ENCODER')
            mp().join(log().errors)
            log().info(time_msg(
                f'[{target_resolution}] PREPROC + ENCODER + VES_MUXER', t1, datetime.now()))
        elif cli.encoder_pass_num == 2:
            # pass 1
            t1 = datetime.now()
            mp().run_multiproc(call, f"[{target_resolution}] PREPROC_PASS1", preproc[0], "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'PREPROC_PASS1')
            windows_sleep()
            mp().run_multiproc(call, f"[{target_resolution}] ENCODER_PASS1", encoder[0], cli.print_all, log().errors, None, mp().lock, decorate, 'ENCODER_PASS1')
            mp().join(log().errors)
            log().info(time_msg(
                f'[{target_resolution}] PREPROC_PASS1 + ENCODER_PASS1', t1, datetime.now()))
            # pass 2
            t1 = datetime.now()
            mp().run_multiproc(call, f"[{target_resolution}] PREPROC_PASS2", preproc[1], "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'PREPROC_PASS2')
            windows_sleep()
            mp().run_multiproc(call, f"[{target_resolution}] VES_MUXER", vesmux, cli.print_all, log().errors, None, mp().lock, decorate, 'VES_MUXER')
            windows_sleep()
            mp().run_multiproc(call, f"[{target_resolution}] ENCODER_PASS2", encoder[1], cli.print_all, log().errors, None, mp().lock, decorate, 'ENCODER_PASS2')
            mp().join(log().errors)
            log().info(time_msg(
                f'[{target_resolution}] PREPROC_PASS2 + ENCODER_PASS2 + VES_MUXER', t1, datetime.now()))

        if postproc:
            if call(f"[{target_resolution}] POSTPROC", postproc, cli.print_all, log().errors, log().infos, None, decorate, 'POSTPROC'):
                raise RuntimeError

        input_path = prefix
        input_path += 'dv_postproc_output_hevc.hevc'if postproc else 'dv_vesmux_output.hevc'

        if cli.dvesverifier:
            verifier = configure_dv_es_verifier(cli, {'input': input_path})
            dv.run_verifier(f'[{target_resolution}] DVESVERIFIER', verifier, 'all', log(
            ).errors, log().infos, None, decorate, 'DVESVERIFIER')

        create_output_file(cli, {'input_path': input_path,
                                 'output_path': output_path,
                                 'log_path': prefix + 'mp4muxer_log().log',
                                 'id': f'[{target_resolution}] MP4_MUXER'})

        windows_sleep()


def data_stream_workflow_parallel(cli, cfg):
    max_output_res = dv.find_max_resolution(cli.target_resolution)
    original_resolution = cfg['input_resolution']
    rpu_pipe_buffer_size = 4096 * cfg['gop_size']
    yuv_pipe_buffer_size = dv.get_pipe_buffer_size(
        cli.yuv_buffer_size, original_resolution)

    encoders = []
    vesmuxers = []
    postprocs = []

    preproc = configure_dv_preproc(cli, {'input_resolution': original_resolution,
                                         'target_resolution': max_output_res,
                                         'gop_size': cfg['gop_size'],
                                         'output_rpu': 'dv_preproc_output_rpu.bin',
                                         'rpu_pipe_buffer_size': rpu_pipe_buffer_size,
                                         'output': 'dv_preproc_output.yuv'})  # list of cmds

    splitter_rpu = configure_splitter(cli, {'block_size': rpu_pipe_buffer_size,
                                            'input_buffer': rpu_pipe_buffer_size,
                                            'output_buffer': rpu_pipe_buffer_size,
                                            'input': 'dv_preproc_output_rpu.bin',
                                            'output': [make_prefix(i, target_res) + 'dv_preproc_output_rpu.bin' for i, target_res in enumerate(cli.target_resolution)]})

    splitter_yuv = configure_splitter(cli, {'block_size': yuv_pipe_buffer_size,
                                            'input_buffer': yuv_pipe_buffer_size,
                                            'output_buffer': yuv_pipe_buffer_size,
                                            'input': 'dv_preproc_output.yuv',
                                            'output': [make_prefix(i, target_res) + 'dv_preproc_output.yuv' for i, target_res in enumerate(cli.target_resolution)]})

    for i, (target_resolution, output_path, data_rate) in enumerate(zip(cli.target_resolution, cli.output, cli.data_rate)):
        prefix = make_prefix(i, target_resolution)
        encoders.append(
            configure_encoder(cli, {'input': prefix + 'dv_preproc_output.yuv',
                                    'gop_size': cfg['gop_size'],
                                    'data_rate': data_rate,
                                    'input_resolution': max_output_res,
                                    'target_resolution': target_resolution,
                                    'frame_rate': cfg['frame_rate'],
                                    'pass_log': prefix + 'ffmpegPassLog',
                                    'output': prefix + 'ffmpeg_output_hevc.hevc'}))  # appending list of cmds

        vesmuxers.append(
            configure_dv_vesmux(cli, {'input_bl': prefix + 'ffmpeg_output_hevc.hevc',
                                'input_rpu': prefix + 'dv_preproc_output_rpu.bin',
                                      'rpu_pipe_buffer_size': rpu_pipe_buffer_size,
                                      'output': prefix + 'dv_vesmux_output.hevc'}))

        postprocs.append(
            configure_dv_postproc(cli, {'input': prefix + 'dv_vesmux_output.hevc',
                                        'input_resolution': max_output_res,
                                        'target_resolution': target_resolution,
                                        'output': prefix + 'dv_postproc_output_hevc.hevc'}))

    t1 = datetime.now()

    if cli.encoder_pass_num == 1:
        # PREPROC PASS 1
        mp().run_multiproc(call, f"[{max_output_res}] PREPROC", preproc[0], "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'PREPROC')
        # SPLITTER YUV
        windows_sleep()
        mp().run_multiproc(call, f"[{max_output_res}] SPLITTER_YUV", splitter_yuv, "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'SPLITTER_YUV')
        # SPLITTER RPU
        mp().run_multiproc(call, f"[{max_output_res}] SPLITTER_RPU", splitter_rpu, "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'SPLITTER_RPU')

        for (encoder, vesmux, resolution) in zip(encoders, vesmuxers, cli.target_resolution):
            # VES_MUXER
            windows_sleep()
            mp().run_multiproc(call, f"[{resolution}] VES_MUXER", vesmux, cli.print_all, log().errors, None, mp().lock, decorate, 'VES_MUXER')
            # ENCODER
            windows_sleep()
            mp().run_multiproc(call, f"[{resolution}] ENCODER", encoder[0], cli.print_all, log().errors, None, mp().lock, decorate, 'ENCODER')
        mp().join(log().errors)
        log().info(time_msg(f'PREPROC + ENCODER + VES_MUXER', t1, datetime.now()))
    elif cli.encoder_pass_num == 2:
        # pass 1
        # PREPROC PASS 1
        mp().run_multiproc(call, f"[{max_output_res}] PREPROC_PASS1", preproc[0], "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'PREPROC_PASS1')
        # SPLITTER YUV
        windows_sleep()
        mp().run_multiproc(call, f"[{max_output_res}] SPLITTER_YUV", splitter_yuv, "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'SPLITTER_YUV')
        for (encoder, resolution) in zip(encoders, cli.target_resolution):
            # ENCODER 1
            windows_sleep()
            mp().run_multiproc(call, f"[{resolution}] ENCODER_PASS1", encoder[0], cli.print_all, log().errors, None, mp().lock, decorate, 'ENCODER_PASS1')
        mp().join(log().errors)
        log().info(time_msg(f'PREPROC_PASS1 + ENCODER_PASS1', t1, datetime.now()))

        # pass 2
        t1 = datetime.now()
        # PREPROC PASS 2
        mp().run_multiproc(call, f"[{target_resolution}] PREPROC_PASS2", preproc[1], "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'PREPROC_PASS2')
        # SPLITTER YUV
        windows_sleep()
        mp().run_multiproc(call, f"[{max_output_res}] SPLITTER_YUV", splitter_yuv, "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'SPLITTER_YUV')
        # SPLITTER RPU
        mp().run_multiproc(call, f"[{max_output_res}] SPLITTER_RPU", splitter_rpu, "all" if cli.progress == 1 else cli.print_all, log().errors, None, mp().lock, decorate, 'SPLITTER_RPU')
        for (encoder, vesmux, resolution) in zip(encoders, vesmuxers, cli.target_resolution):
            # VES_MUXER
            windows_sleep()
            mp().run_multiproc(call, f"[{resolution}] VES_MUXER", vesmux, cli.print_all, log().errors, None, mp().lock, decorate, 'VES_MUXER')
            # ENCODER 2
            windows_sleep()
            mp().run_multiproc(call, f"[{resolution}] ENCODER_PASS2", encoder[1], cli.print_all, log().errors, None, mp().lock, decorate, 'ENCODER_PASS2')
        mp().join(log().errors)
        log().info(time_msg(f'PREPROC_PASS2 + ENCODER_PASS2 + VES_MUXER', t1, datetime.now()))

    if postprocs and postprocs[0]:
        for (postproc, resolution) in zip(postprocs, cli.target_resolution):
            mp().run_multiproc(call, f"[{resolution}] POSTPROC", postproc, cli.print_all, log().errors, None, mp().lock, decorate, 'POSTPROC')
        mp().join(log().errors)

    input_paths = [make_prefix(i, target_res) + ('dv_postproc_output_hevc.hevc' if postproc else 'dv_vesmux_output.hevc') for i, target_res in enumerate(cli.target_resolution)]

    if cli.dvesverifier:
        for (input_path, target_resolution) in zip(input_paths, cli.target_resolution):
            verifier = configure_dv_es_verifier(cli, {'input': input_path})
            dv.run_verifier(f'[{target_resolution}] DVESVERIFIER', verifier, 'all', log().errors, log().infos, None, decorate, 'DVESVERIFIER')

    for i, (input_path, output_path, target_resolution) in enumerate(zip(input_paths, cli.output, cli.target_resolution)):
        create_output_file(cli, {'input_path': input_path,
                                 'output_path': output_path,
                                 'log_path': make_prefix(i, target_resolution) + 'mp4muxer_log().log',
                                 'id': f'[{target_resolution}] MP4_MUXER'})
        windows_sleep()


def file_based_workflow(cli, cfg):
    max_res = dv.find_max_resolution(cli.target_resolution)
    preproc = configure_dv_preproc(cli, {'input_resolution': cfg['input_resolution'],
                                         'target_resolution': max_res,
                                         'gop_size': cfg['gop_size'],
                                         'output_rpu': 'dv_preproc_output_rpu.bin',
                                         'output': 'dv_preproc_output.yuv'})  # list of cmds
    if call(f"[{max_res}] PREPROC", preproc[0], "all" if cli.progress == 1 else cli.print_all, log().errors, log().infos, None, decorate, 'PREPROC'):
        raise RuntimeError

    for i, (target_resolution, output_path, data_rate) in enumerate(zip(cli.target_resolution, cli.output, cli.data_rate)):
        prefix = make_prefix(i, target_resolution)
        encoder = configure_encoder(cli, {'input': 'dv_preproc_output.yuv',
                                          'gop_size': cfg['gop_size'],
                                          'data_rate': data_rate,
                                          'input_resolution': max_res,
                                          'target_resolution': target_resolution,
                                          'frame_rate': cfg['frame_rate'],
                                          'pass_log': prefix + 'ffmpegPassLog',
                                          'output': prefix + 'ffmpeg_output_hevc.hevc'})  # list of cmds

        vesmux = configure_dv_vesmux(cli, {'input_bl': prefix + 'ffmpeg_output_hevc.hevc',
                                           'input_rpu': 'dv_preproc_output_rpu.bin',
                                           'output': prefix + 'dv_vesmux_output.hevc'})

        postproc = configure_dv_postproc(cli, {'input_resolution': max_res,
                                               'target_resolution': target_resolution,
                                               'input': prefix + 'dv_vesmux_output.hevc',
                                               'output': prefix + 'dv_postproc_output_hevc.hevc'})

        for i, cmd in enumerate(encoder):
            decorator_text = f"ENCODER_PASS{i+1}" if len(
                encoder) > 1 else "ENCODER"
            if call(f"[{target_resolution}] " + decorator_text, cmd, "all" if cli.progress == 1 else cli.print_all, log().errors, log().infos, None, decorate, decorator_text):
                raise RuntimeError
        if call(f"[{target_resolution}] VES_MUXER", vesmux, cli.print_all, log().errors, log().infos, None, decorate, 'VES_MUXER'):
            raise RuntimeError

        if postproc:
            if call(f"[{target_resolution}] POSTPROC", postproc, cli.print_all, log().errors, log().infos, None, decorate, 'POSTPROC'):
                raise RuntimeError

        input_path = prefix
        input_path += 'dv_postproc_output_hevc.hevc'if postproc else 'dv_vesmux_output.hevc'

        if cli.dvesverifier:
            verifier = configure_dv_es_verifier(cli, {'input': input_path})
            dv.run_verifier(f'[{target_resolution}] DVESVERIFIER', verifier, 'all', log().errors, log().infos, None, decorate, 'DVESVERIFIER')

        create_output_file(cli, {'input_path': input_path,
                                 'output_path': output_path,
                                 'log_path': prefix + 'mp4muxer_log().log',
                                 'id': f'[{target_resolution}] MP4_MUXER'})


def reconfigure_cli_params(cli, info):
    cli.start, cli.end, cli.duration = dv.reconfigure_start_end_duration(cli.parser.get_default('start'), cli.parser.get_default('end'), cli.parser.get_default('duration'), cli.start, cli.end, cli.duration, info['frame_rate'])
    cli.target_resolution = dv.reconfigure_target_resolution(info['input_resolution'], cli.target_resolution, len(cli.output))
    cli.data_rate = dv.reconfigure_data_rates(cli.data_rate, cli.target_resolution)
    cli.output = dv.reconfigure_output_names(cli.output, cli.data_rate, cli.target_resolution, cli.overwrite)


def run(cli):
    validate(cli)
    info = dv.get_input_info(cli.input, cli.input_format, os.path.join(cli.toolset_dir, 'mediainfo'), os.path.join(cli.toolset_dir, 'dee_dv_parse_mezz'), cli.print_all, cli.license)
    info['gop_size'] = math.floor(math.ceil((info['frame_rate'])) * cli.gop_duration)
    reconfigure_cli_params(cli, info)
    for output in cli.output:
        check_extention(output, ('.h265', '.265', '.mp4'))

    if cli.data_stream and cli.data_stream_arch == 'serial':
        data_stream_workflow(cli, info)
    elif cli.data_stream and cli.data_stream_arch == 'parallel':
        data_stream_workflow_parallel(cli, info)
    else:
        file_based_workflow(cli, info)


def init_cli():
    cli_instance = Cli("Dolby Vision profile 5 encoding workflow.",
                       needs_license=True,
                       needs_concurency=False,
                       needs_progress=True,
                       needs_toolset_dir=True,
                       needs_overwrite=True,
                       needs_print_all=True,
                       needs_temp_dir=True,
                       needs_more_help=True)
    cli_instance.add_option('--ffmpeg', help="FFmpeg executable.", default='ffmpeg')
    cli_instance.add_option('--dvesverifier', help="Dolby Vision ES Verifier executable. If not specified, the script will skip the verification step.", required=False)
    cli_instance.add_option('--input-format', help='Input format followed by format-specific options. Use '
                            '"--morehelp input-format" for more details. Values: '
                            'jpeg2000_list|jpeg2000_mxf|prores_list|prores_mov|tiff_list.', required=True)
    cli_instance.add_option('-i', '--input', help="Input file with mezzanine video. Input directory for list-based inputs.", required=True)
    cli_instance.add_option('-m', '--input-metadata', help="Optional input metadata file. If not specified, the application attempts to extract metadata from the input.", required=False)
    cli_instance.add_option('-o', '--output', help='Space-separated output file paths in HEVC or MP4 format.', required=True, nargs='+')
    cli_instance.add_option('--metadata-offset', help='Offset added to each frame index, when accessing frame metadata from the source.', required=False, default=0, type=int)
    cli_instance.add_option('--start', help="Start position in 'xs' format, where 'x' represents seconds or 'xf' format, where 'x' is a frame number.", required=False, default="0s")

    group = cli_instance.parser.add_mutually_exclusive_group()
    group.add_argument('--duration', help="Duration in 'xs' format, where 'x' represents seconds or 'xf' format, where 'x' is a frame number. '-1' means \"end of file\".", required=False, default="-1")
    group.add_argument('--end', help="End position in 'xs' format, where 'x' represents seconds or 'xf' format, where 'x' is a frame number. '-1' means \"end of file\".", required=False, default="-1")

    cli_instance.add_option('-r', '--target-resolution', help="Output resolution, or space-separated target resolutions in 'WxH' format.", required=False, default=[], nargs='+')
    cli_instance.add_option('--data-rate', help="Target data rate, or space-separated list of target data rates, in kbps units. '0' means \"assign automatically\".", required=False, default=[], type=int, nargs='+')
    cli_instance.add_option('--preset', help='Encoder preset.', choices=['ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'], default='medium')
    cli_instance.add_option('--data-stream', help='Use data streaming instead of temporary files, wherever possible.', choices=[0, 1], type=int, default=0, required=False)
    cli_instance.add_option('--data-stream-arch', help='Data streaming architecture. Relevant only if --data-stream is set to 1.', choices=['serial', 'parallel'], default='serial', required=False)
    cli_instance.add_option('--yuv-buffer-size', help="YUV buffer size in bytes. '-1' means \"assign automatically\". Relevant only if 'data-stream' is enabled.", type=int, default=-1, required=False)
    cli_instance.add_option('-p', '--encoder-pass-num', help='Number of encoder passes.', choices=[1, 2], required=False, default=2, type=int)
    cli_instance.add_option('-g', '--gop-duration', help='GOP duration (seconds). Affects the distance between IDR frames in outputs.', default=2, type=int, required=False)
    cli_instance.add_option('--codec-id', help="Video codec ID. Relevant for MP4 outputs only.", default='dvhe', choices=['dvhe', 'dvh1'])
    cli_instance.add_option('--mdpp', help='Post-processing configuration. Use "--morehelp mdpp" for more details.', choices=['full', 'L5', 'none'], default='full')

    cli_instance.add_more_help('mdpp', get_mdpp_help)
    cli_instance.add_more_help('input-format', dv.get_input_format_help, cli_instance, 'dee_dv5_preproc')
    cli_instance.parse()
    return cli_instance


def main():
    try:
        set_logger(Logger())
        cli = init_cli()
        set_file_ctrl(FileCtrl(cli.temp_dir, cli.keep_temp))
        set_multiproc(MultiProcess())
        run(cli)
        log().execution_summary()
    except RuntimeError:
        log().execution_summary()
        sys.exit(1)


if __name__ == '__main__':
    main()