Skip to content

Instantly share code, notes, and snippets.

@Gronis
Last active January 29, 2024 17:10
Show Gist options
  • Save Gronis/9da3a75f6b3e114d627aadd46052b4cc to your computer and use it in GitHub Desktop.
Save Gronis/9da3a75f6b3e114d627aadd46052b4cc to your computer and use it in GitHub Desktop.
This script takes a hdr10 video and returns a string for x265 encoder with the correct light levels.
#! /usr/bin/env node
// For info about hdr transcoding, go here:
// https://codecalamity.com/encoding-uhd-4k-hdr10-videos-with-ffmpeg/
//
// This script takes a hdr video and returns a string for x265 encoder with the correct light levels like this:
// "hdr-opt=1:repeat-headers=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(11000000,0):max-cll=0,0"
//
const proc = require('child_process');
const exec = (cmd, args) => {
return new Promise((accept, reject) => {
const p = proc.spawn(cmd, args);
let stdout = ""
let stderr = ""
p.on('exit', (code) => {
if(code == 0){
accept({ stdout, stderr} );
} else {
reject({ stdout, stderr} );
}
});
const on_stdout_data = d => stdout += d
const on_stderr_data = d => stderr += d
p.stderr.on('data', on_stderr_data)
p.stdout.on('data', on_stdout_data)
});
}
// Assume denominator is larger than p1 and p2 denominator is that it is divisible with both p1 and p2 denominator
const set_common_denominator = (p1, p2, denominator) => {
const parse = p => {
const [nom_s, denom_s] = p.split('/')
const nom = parseInt(nom_s)
const denom = parseInt(denom_s)
return [nom, denom]
}
const [p1n, p1d] = parse(p1)
const [p2n, p2d] = parse(p2)
const p1mul = denominator / p1d
const p2mul = denominator / p2d
return [ p1n * p1mul, p2n * p2mul ]
}
const hdr10_x265_params = async (file_path) => {
const cmd = 'ffprobe'
const args = [
'-hide_banner', '-loglevel', 'warning', '-select_streams', 'v:0',
'-print_format', 'json', '-show_frames', '-read_intervals', "%+#1", '-show_entries',
"frame=color_space,color_primaries,color_transfer,side_data_list,pix_fmt",
'-i', file_path
]
const { stdout, stderr } = await exec(cmd, args)
console.error(stderr)
const res = JSON.parse(stdout)
// console.log(res)
let hdr_data = res.frames[0]
md_data = hdr_data.side_data_list.find(sd => sd.side_data_type == "Mastering display metadata")
if(!md_data) throw "No HDR metadata";
const red = `R(${set_common_denominator(md_data.red_x, md_data.red_y, 50000).join(',')})`
const green = `G(${set_common_denominator(md_data.green_x, md_data.green_y, 50000).join(',')})`
const blue = `B(${set_common_denominator(md_data.blue_x, md_data.blue_y, 50000).join(',')})`
const white = `WP(${set_common_denominator(md_data.white_point_x, md_data.white_point_y, 50000).join(',')})`
const lum = `L(${set_common_denominator(md_data.max_luminance, md_data.min_luminance, 10000).join(',')})`
cl_data = hdr_data.side_data_list.find(sd => sd.side_data_type == "Content light level metadata")
const colorprim = hdr_data.color_primaries
const transfer = hdr_data.color_transfer
const colormatrix = hdr_data.color_space
const master_display = green + blue + red + white + lum
const max_cll = !cl_data? '0,0' : `${cl_data.max_content},${cl_data.max_average}`
return `hdr-opt=1:repeat-headers=1:colorprim=${colorprim}:transfer=${transfer}:colormatrix=${colormatrix}:master-display=${master_display}:max-cll=${max_cll}`
}
const main = async () => {
const file_path = process.argv[2]
console.log(await hdr10_x265_params(file_path))
}
main()

How to use

Example:

ffmpeg -i IN_FILE.mkv -vf scale=1920:1080 -map 0:v -c:v libx265 -pix_fmt yuv420p10le -preset slow -crf 18 -x265-params "rc-lookahead=120:bframes=8:psy-rd=1.5:psy-rdoq=3:aq-mode=3:aq-strength=1.0:ref=6:lookahead-slices=6:max-merge=5:deblock=-1,-1:aq-motion=1:$(node hdr10conversion.js IN_FILE.mkv)" OUT_FILE.mkv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment