Files
2022-02-22 16:42:05 +01:00

182 lines
5.2 KiB
Python
Executable File

#! /usr/bin/env python3
import youtube_dl
import json
import ffmpeg
import subprocess
import glob
import sys
import argparse
import os
# bitrate recommendations as from https://developers.google.com/media/vp9/settings/vod
bitrateProfiles = {
"240": {"heigth": 240, "bitrate": "150k", "minrate": "75k", "maxrate": "218k"},
"360": {"heigth": 360, "bitrate": "276k", "minrate": "138k", "maxrate": "400k"},
"480LQ": {"heigth": 480, "bitrate": "512k", "minrate": "256k", "maxrate": "742k"},
"480MQ": {"heigth": 480, "bitrate": "750k", "minrate": "375k", "maxrate": "1088k"},
"720@30": {"heigth": 720, "bitrate": "1024k", "minrate": "512k", "maxrate": "1485k"},
"720@60": {"heigth": 720, "bitrate": "1800k", "minrate": "900k", "maxrate": "2610k"},
"1080@30": {"heigth": 1080, "bitrate": "1800k", "minrate": "900k", "maxrate": "2610k"},
"1080@60": {"heigth": 1080, "bitrate": "3000k", "minrate": "1500k", "maxrate": "4350k"},
"1440@30": {"heigth": 1440, "bitrate": "6000k", "minrate": "3000k", "maxrate": "8700k"},
"1440@60": {"heigth": 1440, "bitrate": "9000k", "minrate": "4500k", "maxrate": "13050k"},
"2160@30": {"heigth": 2160, "bitrate": "12000k", "minrate": "6000k", "maxrate": "17400k"},
"2160@60": {"heigth": 2160, "bitrate": "18000k", "minrate": "9000k", "maxrate": "26100k"},
}
config = {
'vcodec': "vp9",
'acodec': "libopus",
'bitrateProfile': "480LQ",
'quality': "best",
}
def generate_thumbnail(in_filename, out_filename, time, height):
try:
(
ffmpeg
.input(in_filename, ss=time)
.filter('scale', -2, height)
.output(out_filename, vframes=1)
.overwrite_output()
.run(capture_stdout=True, capture_stderr=True)
)
except ffmpeg.Error as e:
print(e.stderr.decode(), file=sys.stderr)
sys.exit(1)
# setting width and height from hardcoded defaults -> configured defaults -> clip
def getDimensions(config, clip, bitrateProfiles):
# set width and height to a default
w, h = -2, 480
# set the height by the bitrateProfile
try:
h = bitrateProfiles[config['bitrateProfile']]['height']
except:
pass
# overwrite it with the scaling
if 'scale' in clip:
h = clip['scale']['h'] if 'h' in clip['scale'] else h
w = clip['scale']['w'] if 'w' in clip['scale'] else w
return w, h
def twoPassEncode(inputFilename, clip):
if os.path.isfile(clip['target']):
print(f"{clip['target']} already existing! Skipping!")
return
# cutting
kwArgs = {}
if 'from' in clip:
kwArgs['ss'] = clip['from']
if 'to' in clip:
kwArgs['to'] = clip['to']
stream = ffmpeg.input( inputFilename, **kwArgs)
video, audio = stream.video, stream.audio
if 'crop' in clip:
stream = ffmpeg.filter(stream,
"crop",
x=clip['crop']['x'],
y=clip['crop']['y'],
w=clip['crop']['w'],
h=clip['crop']['h']
)
stream = ffmpeg.output(stream,
# clip['target'],
"/dev/null",
vcodec=config['vcodec'],
**{
"an": None,
"y": None,
"f": "null",
"pass": "1",
"b:v": bitrateProfiles[config['bitrateProfile']]['bitrate'],
"minrate": bitrateProfiles[config['bitrateProfile']]['minrate'],
"maxrate": bitrateProfiles[config['bitrateProfile']]['maxrate'],
"quality": config['quality'] if 'quality' in config else "best",
}
)
try:
ffmpeg.run(stream)
except:
print(infoDict)
if 'from' in clip and 'to' in clip:
stream = ffmpeg.input(
glob.glob(infoDict['id']+"*")[0],
ss=clip['from'],
to=clip['to'],
)
else:
stream = ffmpeg.input(
glob.glob(infoDict['id']+"*")[0]
)
if 'crop' in clip:
stream = ffmpeg.filter(stream,
"crop",
x=clip['crop']['x'],
y=clip['crop']['y'],
w=clip['crop']['w'],
h=clip['crop']['h']
)
h, w = getDimensions(config, clip, bitrateProfiles)
stream = ffmpeg.output(stream, audio,
clip['target'],
vcodec=config['vcodec'],
**{
"pass": "2",
"b:v": bitrateProfiles[config['bitrateProfile']]['bitrate'],
"minrate": bitrateProfiles[config['bitrateProfile']]['minrate'],
"maxrate": bitrateProfiles[config['bitrateProfile']]['maxrate'],
"quality": config['quality'] if 'quality' in config else "best",
"acodec": config['acodec'],
}
)
try:
ffmpeg.run(stream)
except:
print(infoDict)
argParser = argparse.ArgumentParser()
jsonFileName = sys.argv[1]
clipDict = {}
with open(jsonFileName) as jf:
clipDict = json.load(jf)
ydl_opts = {"outtmpl": "%(id)s"}
for clip in clipDict:
# create the directories so ffmpeg doesn't complain
try:
outputDir = os.path.dirname(clip['target'])
os.makedirs(outputDir)
except:
print(f"Couldn't create {outputDir}")
infoDict = None
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
infoDict = ydl.extract_info(clip['source'], download=False)
ydl.download([clip['source']])
if infoDict is not None:
# @todo This is a very bad hack because the outtmpl options doesn't seem to be working if the file gets reencoded
inputFilename = glob.glob(infoDict['id']+"*")[0]
w, h = getDimensions(config, clip, bitrateProfiles)
# generate preview image for the video
if 'poster' in clip:
generate_thumbnail(inputFilename, os.path.splitext(clip['target'])[0]+".jpg", clip['poster']['timeIndex'], h )
twoPassEncode(inputFilename, clip)