impl disposition rewrite
This commit is contained in:
92
bin/ffx.py
92
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)
|
||||
|
||||
subtitleIndex = 0
|
||||
for subtitleStreamDescriptor in streamDescriptor[STREAM_TYPE_SUBTITLE]:
|
||||
mappingTokens += ['-map', f"s:{subtitleIndex}"]
|
||||
subtitleIndex += 1
|
||||
if default_subtitle == -1:
|
||||
sourceSubtitleStreams = streamDescriptor[STREAM_TYPE_SUBTITLE]
|
||||
else:
|
||||
sourceSubtitleStreams = getReorderedSubstreams(streamDescriptor[STREAM_TYPE_SUBTITLE], default_subtitle)
|
||||
dispositionTokens += generateDispositionTokens(sourceSubtitleStreams)
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user