You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ffx/bin/ffx.py

236 lines
6.6 KiB
Python

#! /usr/bin/python3
import os, sys, subprocess, json
DEFAULT_QUALITY = 23
DEFAULT_AV1_PRESET = 5
DEFAULT_STEREO_BANDWIDTH = "128"
DEFAULT_AC3_BANDWIDTH = "256"
DEFAULT_DTS_BANDWIDTH = "320"
TEMP_FILE_NAME = "ffmpeg2pass-0.log"
def executeProcess(commandSequence):
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
return output
def generateAV1Tokens(q, p):
return ['-c:v:0',
'libsvtav1',
'-svtav1-params',
f"crf={q}:preset={p}:tune=0:enable-overlays=1:scd=1:scm=0",
'-pix_fmt', 'yuv420p10le']
def generateVP9Pass1Tokens(q):
return ['-c:v:0', 'libvpx-vp9', '-row-mt', '1',
'-crf', str(q), '-pass', '1', '-speed', '4',
'-frame-parallel', '0', '-g', '9999', '-aq-mode', '0']
def generateVP9Pass2Tokens(q):
return ['-c:v:0', 'libvpx-vp9', '-row-mt', '1',
'-crf', str(q), '-pass', '2',
'-frame-parallel', '0', '-g', '9999', '-aq-mode', '0',
'-auto-alt-ref', '1', '-lag-in-frames', '25']
def generateCropTokens(start, length):
return ['-ss', str(start), '-t', str(length)]
def generateOutputTokens(f, q=''):
if q:
fTokens = f.split('.')
paddedFilename = '.'.join(fTokens[:-1]) + f" q{q}" + '.' + fTokens[-1]
return ['-f', 'webm', paddedFilename]
else:
return ['-f', 'webm', f]
inputFilename = sys.argv[1]
outputFilename = sys.argv[2]
targetFormat = 'vp9'
if 'av1' in sys.argv:
targetFormat = 'av1'
if 'vp9' in sys.argv:
targetFormat = 'vp9'
qualities = [str(DEFAULT_QUALITY)]
qualitiesTokens = [q for q in sys.argv if q.startswith('q=')]
if qualitiesTokens:
qualitiesString = qualitiesTokens[0].split('=')[1]
qualities = qualitiesString.split(',')
preset = DEFAULT_AV1_PRESET
presetTokens = [p for p in sys.argv if p.startswith('p=')]
if presetTokens:
preset = int(presetTokens[0].split('=')[1])
stereoBandwidth = DEFAULT_STEREO_BANDWIDTH
stereoTokens = [s for s in sys.argv if s.startswith('a=')]
if stereoTokens:
stereoBandwidth = str(stereoTokens[0].split('=')[1])
if not stereoBandwidth.endswith('k'):
stereoBandwidth += "k"
ac3Bandwidth = DEFAULT_AC3_BANDWIDTH
ac3Tokens = [a for a in sys.argv if a.startswith('ac3=')]
if ac3Tokens:
ac3Bandwidth = str(ac3Tokens[0].split('=')[1])
if not ac3Bandwidth.endswith('k'):
ac3Bandwidth += "k"
dtsBandwidth = DEFAULT_DTS_BANDWIDTH
dtsTokens = [d for d in sys.argv if d.startswith('dts=')]
if dtsTokens:
dtsBandwidth = str(dtsTokens[0].split('=')[1])
if not dtsBandwidth.endswith('k'):
dtsBandwidth += "k"
cropStart = ''
cropLength = ''
cropTokens = [c for c in sys.argv if c.startswith('crop')]
if cropTokens:
if '=' in cropTokens[0]:
cropString = cropTokens[0].split('=')[1]
cropStart, cropLength = cropString.split(',')
else:
cropStart = 60
cropLength = 180
output = executeProcess(["ffprobe", "-show_streams", "-of", "json" ,inputFilename])
streamData = json.loads(output)['streams']
videoStreams = [s for s in streamData if s['codec_type'] == 'video']
audioStreams = [s for s in streamData if s['codec_type'] == 'audio']
subtitleStreams = [s for s in streamData if s['codec_type'] == 'subtitle']
for aStream in audioStreams:
if 'channel_layout' in aStream.keys():
print(f"audio stream: {aStream['channel_layout']}") #channel_layout
else:
print(f"unknown audio stream with {aStream['channels']} channels") #channel_layout
commandTokens = ['ffmpeg', '-y', '-i', inputFilename]
mappingTokens = ['-map', 'v:0']
for a in range(len(audioStreams)):
mappingTokens += ['-map', f"a:{a}"]
for s in range(len(subtitleStreams)):
mappingTokens += ['-map', f"s:{s}"]
audioTokens = []
audioStreamIndex = 0
for aStream in audioStreams:
channels = aStream['channels']
if 'channel_layout' in aStream.keys():
channelLayout = aStream['channel_layout']
if channelLayout == '6.1':
audioTokens += [f"-c:a:{audioStreamIndex}",
'libopus',
f"-filter:a:{audioStreamIndex}",
'channelmap=channel_layout=6.1',
f"-b:a:{audioStreamIndex}",
dtsBandwidth]
if channelLayout == '5.1(side)':
audioTokens += [f"-c:a:{audioStreamIndex}",
'libopus',
f"-filter:a:{audioStreamIndex}",
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
f"-b:a:{audioStreamIndex}",
ac3Bandwidth]
if channelLayout == 'stereo':
audioTokens += [f"-c:a:{audioStreamIndex}",
'libopus',
f"-b:a:{audioStreamIndex}",
stereoBandwidth]
else:
if channels == 6:
audioTokens += [f"-c:a:{audioStreamIndex}",
'libopus',
f"-b:a:{audioStreamIndex}",
ac3Bandwidth]
audioStreamIndex += 1
nullTokens = ['-f', 'null', '/dev/null']
for quality in qualities:
if targetFormat == 'av1':
commandSequence = commandTokens + mappingTokens + generateAV1Tokens(quality, preset) + audioTokens
if cropStart:
commandSequence += generateCropTokens(cropStart, cropLength)
if len(qualities) > 1:
commandSequence += generateOutputTokens(outputFilename, quality)
else:
commandSequence += generateOutputTokens(outputFilename)
print(f"Command: {' '.join(commandSequence)}")
executeProcess(commandSequence)
if targetFormat == 'vp9':
commandSequence1 = commandTokens + mappingTokens + generateVP9Pass1Tokens(quality)
if cropStart:
commandSequence1 += generateCropTokens(cropStart, cropLength)
commandSequence1 += nullTokens
print(f"Command 1: {' '.join(commandSequence1)}")
if os.path.exists(TEMP_FILE_NAME):
os.remove(TEMP_FILE_NAME)
executeProcess(commandSequence1)
commandSequence2 = commandTokens + mappingTokens + generateVP9Pass2Tokens(quality) + audioTokens
if cropStart:
commandSequence2 += generateCropTokens(cropStart, cropLength)
if len(qualities) > 1:
commandSequence2 += generateOutputTokens(outputFilename, quality)
else:
commandSequence2 += generateOutputTokens(outputFilename)
print(f"Command 2: {' '.join(commandSequence2)}")
executeProcess(commandSequence2)