From dcd79b74fda1fced99a77643a1be343f90d66536 Mon Sep 17 00:00:00 2001 From: Maveno Date: Sat, 14 Sep 2024 13:03:47 +0200 Subject: [PATCH] impl disposition rewrite --- bin/ffx.py | 92 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/bin/ffx.py b/bin/ffx.py index 6ecd3b1..9ee17ef 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -12,7 +12,6 @@ VERSION='0.1.0' DEFAULT_VIDEO_ENCODER = 'vp9' DEFAULT_QUALITY = 23 - DEFAULT_AV1_PRESET = 5 DEFAULT_FILE_FORMAT = 'webm' @@ -230,10 +229,30 @@ def getStreamDescriptor(filename): s['layout'] = 'undefined' descriptor[s['codec_type']].append(s) + descriptor[s['codec_type']][-1]['src_sub_index'] = len(descriptor[s['codec_type']]) - 1 return descriptor +def getModifiedStreamOrder(length, last): + """This is jellyfin specific as the last stream in the order is set as default""" + seq = list(range(length)) + if last < 0 or last > length -1: + return seq + seq.pop(last) + seq.append(last) + return seq + + +def getReorderedSubstreams(subDescriptor, last): + numSubStreams = len(subDescriptor) + modifiedOrder = getModifiedStreamOrder(numSubStreams, last) + reorderedDescriptor = [] + for streamIndex in range(numSubStreams): + reorderedDescriptor.append(subDescriptor[modifiedOrder[streamIndex]]) + return reorderedDescriptor + + def generateAV1Tokens(q, p): return ['-c:v:0', 'libsvtav1', @@ -321,6 +340,25 @@ def generateClearTokens(streams): return clearTokens +def generateDispositionTokens(subDescriptor): + """-disposition:s:X default+forced""" + + dispositionTokens = [] + + for subStreamIndex in range(len(subDescriptor)): + + subStream = subDescriptor[subStreamIndex] + streamType = subStream['codec_type'][0] # v|a|s + dispositionFlags = {k for (k,v) in subStream['disposition'].items() if v == 1} if 'disposition' in subStream.keys() else set() + + if dispositionFlags: + dispositionTokens += [f"-disposition:{streamType}:{subStreamIndex}", '+'.join(dispositionFlags)] + else: + dispositionTokens += [f"-disposition:{streamType}:{subStreamIndex}", '0'] + + return dispositionTokens + + @click.group() @click.pass_context def ffx(ctx): @@ -370,10 +408,10 @@ def streams(filename): @click.option('-ac3', '--ac3-bitrate', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {DEFAULT_AC3_BANDWIDTH})") @click.option('-dts', '--dts-bitrate', type=int, default=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {DEFAULT_DTS_BANDWIDTH})") -@click.option('-ds', '--default-subtitle', type=int, help='Index of default subtitle stream') +@click.option('-ds', '--default-subtitle', type=int, default=-1, help='Index of default subtitle stream') -@click.option('-fa', '--forced-audio', type=int, help='Index of forced audio stream (including default audio stream tag)') -@click.option('-da', '--default-audio', type=int, help='Index of default audio stream') +@click.option('-fa', '--forced-audio', type=int, default=-1, help='Index of forced audio stream (including default audio stream tag)') +@click.option('-da', '--default-audio', type=int, default=-1, help='Index of default audio stream') @click.option("--crop", is_flag=False, flag_value="default", default="none") @@ -384,10 +422,12 @@ def streams(filename): @click.option("-c", "--clear-metadata", is_flag=True, default=False) @click.option("-d", "--denoise", is_flag=True, default=False) +@click.option("-j", "--no-jellyfin-tweaks", is_flag=True, default=False) + @click.option("--dry-run", is_flag=True, default=False) -def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, ac3_bitrate, dts_bitrate, default_subtitle, forced_audio, default_audio, crop, output_directory, clear_metadata, denoise, dry_run): +def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, ac3_bitrate, dts_bitrate, default_subtitle, forced_audio, default_audio, crop, output_directory, clear_metadata, denoise, no_jellyfin_tweaks, dry_run): """Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin Files found under PATHS will be converted according to parameters. @@ -483,6 +523,7 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a print(f"season={season} episode={episode} file={file_index}") + # File specific tokens targetFilenameTokens = [] targetFilenameExtension = DEFAULT_FILE_EXTENSION @@ -522,22 +563,34 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a job_index += 1 mappingVideoTokens = ['-map', 'v:0'] + mappingTokens = mappingVideoTokens.copy() + dispositionTokens = [] + audioTokens = [] - audioIndex = 0 - for audioStreamDescriptor in streamDescriptor[STREAM_TYPE_AUDIO]: - mappingTokens += ['-map', f"a:{audioIndex}"] - audioTokens += generateAudioTokens(context, audioIndex, audioStreamDescriptor['layout']) - audioIndex += 1 + if default_audio == -1: + sourceAudioStreams = streamDescriptor[STREAM_TYPE_AUDIO] + else: + sourceAudioStreams = getReorderedSubstreams(streamDescriptor[STREAM_TYPE_AUDIO], default_audio) + dispositionTokens += generateDispositionTokens(sourceAudioStreams) + + if default_subtitle == -1: + sourceSubtitleStreams = streamDescriptor[STREAM_TYPE_SUBTITLE] + else: + sourceSubtitleStreams = getReorderedSubstreams(streamDescriptor[STREAM_TYPE_SUBTITLE], default_subtitle) + dispositionTokens += generateDispositionTokens(sourceSubtitleStreams) - subtitleIndex = 0 - for subtitleStreamDescriptor in streamDescriptor[STREAM_TYPE_SUBTITLE]: - mappingTokens += ['-map', f"s:{subtitleIndex}"] - subtitleIndex += 1 + for audioStream in sourceAudioStreams: + mappingTokens += ['-map', f"a:{audioStream['src_sub_index']}"] + audioTokens += generateAudioTokens(context, audioStream['src_sub_index'], audioStream['layout']) + for subtitleStream in sourceSubtitleStreams: + mappingTokens += ['-map', f"s:{subtitleStream['src_sub_index']}"] + + # Job specific tokens targetFilenameJobTokens = targetFilenameTokens.copy() if len(q_list) > 1: @@ -552,10 +605,13 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a click.echo(f"target filename: {targetFilename}") - if video_encoder == 'av1': - commandSequence = commandTokens + mappingTokens + audioTokens + generateAV1Tokens(q, preset) + audioTokens + commandSequence = (commandTokens + + mappingTokens + + dispositionTokens + + audioTokens + + generateAV1Tokens(q, preset) + audioTokens) if clear_metadata: commandSequence += generateClearTokens(streamDescriptor) @@ -589,7 +645,9 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a executeProcess(commandSequence1) - commandSequence2 = commandTokens + mappingTokens + commandSequence2 = (commandTokens + + mappingTokens + + dispositionTokens) if denoise: commandSequence2 += generateDenoiseTokens()