UnixTools/plexoptimise.py
2020-06-01 17:19:42 +02:00

171 lines
6.1 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from collections import OrderedDict
from json import loads
from pprint import pprint
import subprocess
import sys
# Found: https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg
# and http://forum.doom9.org/showthread.php?t=168267
ATSC_MODE="pan=stereo|FL<1.0*FL+0.707*FC+0.707*BL|FR<1.0*FR+0.707*FC+0.707*BR"
NIGHT_MODE="pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR"
STEREO="pan=stereo|c0=FL|c1=FR"
FIRST_LANGUAGE="eng" # first audio language
DEFAULT_LANGUAGE="eng" # assume this language if not set in metadata
CRF="24" # 18 - almost lossless, 22 - medium, 28 - low
KEEP_VIDEO=["h264"]
# https://stackoverflow.com/questions/3844430/how-to-get-the-duration-of-a-video-in-python
def ffprobe(filepath):
command = [
"ffprobe",
"-loglevel", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
filepath
]
pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, err = pipe.communicate()
return loads(out)
def add_video_params(params, out_idx, stream):
params["map"].append("0:{}".format(stream["index"]))
if int(stream["height"]) > 720 or stream["codec_name"] != "h264":
params["c:v"] = "h264"
params["preset"] = "fast"
params["crf"] = CRF
# TODO: Might need some tweaking, not sure if it's really "mpeg4"
if stream["codec_name"] == "mpeg4":
params["bsf:v"] = "mpeg4_unpack_bframes"
if int(stream["height"]) > 720:
# Correct aspect ratio and rescale to 720
params["filter_complex"].append("[0:{}]scale=iw*sar:ih,scale=-2:720,setsar=1:1".format(stream["index"]))
else:
# Just correct aspect ratio if needed
params["filter_complex"].append("[0:{}]scale=iw*sar:ih,setsar=1:1".format(stream["index"]))
else:
params["filter_complex"].append("[0:{}]null".format(stream["index"]))
params["c:v"] = "copy"
params["movflags"] = "+faststart"
if "field_order" in stream and stream["field_order"] != "progressive":
fc = params["filter_complex"].pop()
fc += ",yadif=1" # 1 = use detected field_order, doubles framerate / 0 = merge both fields into frame, keeps framerate
# To force field_order: 1:0 (top field first) or 1:1 (bottom field first) / -field-dominance 0 or -field-dominance 1
params["filter_complex"].append(fc)
# Force progressive: setfield=mode=prog
def add_audio_params(params, out_idx, stream):
params["map"].append("0:{}".format(stream["index"]))
params["c:a"] = "aac"
params["b:a"] = "96k"
params["ar"] = "44100"
params["ac"] = "2"
if stream["channel_layout"] != "stereo":
params["filter_complex"].append("[0:{}]{}".format(stream["index"], NIGHT_MODE))
else:
params["filter_complex"].append("[0:{}]{}".format(stream["index"], STEREO))
lang = stream["tags"]["language"]
if lang in ["und"]:
print("Language fallback from \"{}\" to \"{}\".".format(lang, DEFAULT_LANGUAGE), file=sys.stderr)
lang = DEFAULT_LANGUAGE
params["metadata:s:{}".format(out_idx)] = "language={}".format(lang)
def add_sub_params(params, out_idx, stream):
pass
def build_ffmpeg_cmd(filename, params):
cmd = ["nice", "ffmpeg", "-i", "\"{}\"".format(filename)]
for p, v in params.items():
if p == "map":
for s in v:
cmd.append("-{}".format(p))
cmd.append(s)
continue
cmd.append("-{}".format(p))
if p == "filter_complex":
cmd.append("\"{}\"".format(";".join(v)))
else:
cmd.append(v)
cmd.append("\"{}.mkv\"".format(filename))
return cmd
def get_optimise_cmdline(filename):
data = ffprobe(filename)
#pprint(data)
video_streams = []
audio_streams = []
other_streams = []
for s in data["streams"]:
codec = s["codec_type"]
if not "tags" in s:
s["tags"] = {
"language": DEFAULT_LANGUAGE
}
elif not "language" in s["tags"]:
s["tags"]["language"] = DEFAULT_LANGUAGE
if codec == "video":
video_streams.append(s)
print("Input #{}: Video {} {}x{}{}".format(
s["index"],
s["codec_name"],
s["width"],
s["height"],
"p" if "field_order" not in s or s["field_order"] == "progressive" else "i"
), file=sys.stderr)
elif codec == "audio":
audio_streams.append(s)
print("Input #{}: Audio {} {}ch {} ({})".format(
s["index"],
s["codec_name"],
s["channels"],
s["channel_layout"],
s["tags"]["language"]
), file=sys.stderr)
else:
other_streams.append(s)
print("Input #{}: Data {} {}".format(
s["index"],
s["codec_type"],
s["codec_tag_string"]
), file=sys.stderr)
# Make sure first audio is FIRST_LANGUAGE
if audio_streams[0]["tags"]["language"] != FIRST_LANGUAGE:
for i, s in enumerate(audio_streams):
if s["tags"]["language"] == FIRST_LANGUAGE:
del audio_streams[i]
audio_streams.insert(0, s)
break
output_streams = video_streams + audio_streams + other_streams
# Now process streams in order to build command line
params = OrderedDict({
"map": [],
"filter_complex": []
})
for i, s in enumerate(output_streams):
codec = s["codec_type"]
if codec == "video":
add_video_params(params, i, s)
elif codec == "audio":
add_audio_params(params, i, s)
elif codec == "subtitle":
add_sub_params(params, i, s)
else:
print("Unknown codec_type: {}".format(codec), file=sys.stderr)
cmd = build_ffmpeg_cmd(filename, params)
#pprint(output_streams)
return " ".join(cmd)
for f in sys.argv[1:]:
print(get_optimise_cmdline(f))