#!/usr/bin/env python3 import os import time import csv import ffmpy from multiprocessing import cpu_count from collections import OrderedDict import json from typing import Union, Any # encoding options used encoding: dict[str, Any] = { "libx264": { "crf": [10, 14, 16, 18, 20, 22, 25], "presets": [ "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow", ], }, "libx265": { "crf": [10, 14, 16, 18, 20, 22, 25], "presets": [ "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow", ], }, "libaom-av1": {"crf": [20, 25, 30, 35, 40], "presets": [0, 2, 4, 6]}, "libsvtav1": {"crf": [20, 25, 30, 35, 40], "presets": [0, 4, 8, 12]}, } # program version # make tests reproducible by tag version = "v0.0.1a2" # always round timestamp to integer def now(): return int(time.time()) def write_line( codec: str, crf: int, preset: Union[str, int], infile: str, outfilesize: float, enctime: int, vmafmean: float, vmafmin: float, ): """ Write line to data csv Parameters: codec (str): Codec used crf (int): CRF used preset (str/int): Preset used infile (str): Input file name outfilesize (float): Size of output file enctime (int): Time to encode vmafmean (float): Mean VMAF score vmafmin (float): Min VMAF score """ with open(datafile, "a", newline="") as file: write = csv.writer(file) write.writerow( ( codec, crf, preset, infile, outfilesize, enctime, vmafmean, vmafmin, ) ) def encode_general( inputfile: str, outputfile: str, codec: str, crf: int, preset: Union[str, int] ): """ General encoding function Parameters: inputfile (str): Path to input file outputfile (str): Path to output file codec (str): Codec used crf (int): CRF value preset (str/int): Choosen preset """ ff = ffmpy.FFmpeg( inputs={inputfile: None}, outputs={ outputfile: "-c:v {videocodec} -crf {crf} -preset {preset} -g 240 -map 0:v:0 ".format( videocodec=codec, crf=crf, preset=preset, ) }, ) return ff def encode_libaom(inputfile: str, outputfile: str, crf: int, preset: Union[str, int]): """ Encoding with libaom Parameters: inputfile (str): Path to input file outputfile (str): Path to output file crf (int): CRF value preset (str/int): Choosen preset """ ff = ffmpy.FFmpeg( inputs={inputfile: None}, outputs={ outputfile: "-c:v libaom-av1 -crf {crf} -b:v 0 -cpu-used {preset} -row-mt 1 -tiles 2x2 -g 240 -map 0:v:0 ".format( crf=crf, preset=preset, ) }, ) return ff def score_vmaf(outputfile: str, inputfile: str) -> dict[str, float]: """ Calculate a file's VMAF score. Higher is better Parameters: outputfile (str): Path to output file inputfile (str): Path to input file Return: dict[str, float]: VMAF mean and min value """ ff = ffmpy.FFmpeg( inputs=OrderedDict([(outputfile, None), (inputfile, None)]), outputs={ "-": "-filter_complex libvmaf=log_fmt=json:n_threads={cputhreads}:log_path=vmaf.json -f null".format( cputhreads=cpu_count() ) }, ) ff.run() with open("vmaf.json", "r") as file: vmafall = json.load(file) vmaf: dict[str, float] = { "mean": vmafall["pooled_metrics"]["vmaf"]["mean"], "min": vmafall["pooled_metrics"]["vmaf"]["min"], } return vmaf def score_psnr(outputfile: str, inputfile: str): """ Calculate a file's MSE (mean-square error) using PSNR. A lower value is better Parameters: outputfile (str): Path to output file inputfile (str): Path to input file Return: TBD """ ff = ffmpy.FFmpeg( inputs=OrderedDict([(outputfile, None), (inputfile, None)]), outputs={"-": "-lavfi psnr=stats_file=psnr.log -f null"}, ) ff.run() # Steps to get mse value def score_ssim(outputfile: str, inputfile: str): """ Calculate a file's SSIM rating. TBD Parameters: outputfile (str): Path to output file inputfile (str): Path to input file Return: TBD """ ff = ffmpy.FFmpeg( inputs=OrderedDict([(outputfile, None), (inputfile, None)]), outputs={"-": "-lavfi ssim=stats_file=ssim.log -f null"}, ) ff.run() # Steps to get ssim value if __name__ == "__main__": if not os.path.isdir("encoded"): os.mkdir("encoded") datafile = version + "-data-" + str(now()) + ".csv" with open(datafile, "w", newline="") as file: write = csv.writer(file) write.writerow( ( "Codec", "CRF", "Preset", "Input file", "Output file size (MiB)", "Encode time (s)", "VMAF Score (mean)", "VMAF Score (min)", ) ) inputfile = "Sparks_in_Blender.webm" for codec in encoding: for crf in encoding[codec]["crf"]: for preset in encoding[codec]["presets"]: outputfile = os.path.join( "encoded", ( "Sparks_in_Blender-codec_" + codec + "-crf_" + str(crf) + "-preset_" + str(preset) + ".mkv" ), ) # libaom needs additional options if codec == "libaom-av1": ff = encode_libaom( inputfile=inputfile, outputfile=outputfile, crf=crf, preset=preset, ) else: ff = encode_general( inputfile=inputfile, outputfile=outputfile, codec=codec, crf=crf, preset=preset, ) # execute previously defined encoding settings starttime = now() ff.run() endtime = now() difftime = int(endtime - starttime) outputfilesize = os.path.getsize(outputfile) / 1024 / 1024 vmaf = score_vmaf(outputfile=outputfile, inputfile=inputfile) write_line( codec=codec, crf=crf, preset=preset, infile="Sparks_in_Blender.webm", outfilesize=outputfilesize, enctime=difftime, vmafmean=vmaf["mean"], vmafmin=vmaf["min"], ) os.remove(outputfile)