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