#! /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)