271 lines
10 KiB
Go
271 lines
10 KiB
Go
package encoding
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// CRFH264 is the Constant Rate Factor for H.264 encoding (lower = higher quality, range 0-51)
|
|
CRFH264 = 15
|
|
// CRFAV1 is the Constant Rate Factor for AV1 encoding (lower = higher quality, range 0-63)
|
|
CRFAV1 = 30
|
|
// CRFVP9 is the Constant Rate Factor for VP9 encoding (lower = higher quality, range 0-63)
|
|
CRFVP9 = 30
|
|
)
|
|
|
|
// tonemapFilter returns the appropriate filter for EXR input.
|
|
// For HDR preservation: converts linear RGB (EXR) to bt2020 YUV with HLG transfer function
|
|
// Uses zscale to properly convert colorspace from linear RGB to bt2020 YUV while preserving HDR range
|
|
// Step 1: Ensure format is gbrpf32le (linear RGB)
|
|
// Step 2: Convert transfer function from linear to HLG (arib-std-b67) with bt2020 primaries/matrix
|
|
// Step 3: Convert to YUV format
|
|
func tonemapFilter(useAlpha bool) string {
|
|
// Convert from linear RGB (gbrpf32le) to HLG with bt709 primaries to match PNG appearance
|
|
// Based on best practices: convert linear RGB directly to HLG with bt709 primaries
|
|
// This matches PNG color appearance (bt709 primaries) while preserving HDR range (HLG transfer)
|
|
// zscale uses numeric values:
|
|
// primaries: 1=bt709 (matches PNG), 9=bt2020
|
|
// matrix: 1=bt709, 9=bt2020nc, 0=gbr (RGB input)
|
|
// transfer: 8=linear, 18=arib-std-b67 (HLG)
|
|
// Direct conversion: linear RGB -> HLG with bt709 primaries -> bt2020 YUV (for wider gamut metadata)
|
|
// The bt709 primaries in the conversion match PNG, but we set bt2020 in metadata for HDR displays
|
|
// Convert linear RGB to sRGB first, then convert to HLG
|
|
// This approach: linear -> sRGB -> HLG -> bt2020
|
|
// Fixes red tint by using sRGB conversion, preserves HDR range with HLG
|
|
filter := "format=gbrpf32le,zscale=transferin=8:transfer=13:primariesin=1:primaries=1:matrixin=0:matrix=1:rangein=full:range=full,zscale=transferin=13:transfer=18:primariesin=1:primaries=9:matrixin=1:matrix=9:rangein=full:range=full"
|
|
if useAlpha {
|
|
return filter + ",format=yuva420p10le"
|
|
}
|
|
return filter + ",format=yuv420p10le"
|
|
}
|
|
|
|
// SoftwareEncoder implements software encoding (libx264, libaom-av1, libvpx-vp9).
|
|
type SoftwareEncoder struct {
|
|
codec string
|
|
}
|
|
|
|
func (e *SoftwareEncoder) Name() string { return "software" }
|
|
func (e *SoftwareEncoder) Codec() string { return e.codec }
|
|
|
|
func (e *SoftwareEncoder) Available() bool {
|
|
return true // Software encoding is always available
|
|
}
|
|
|
|
func (e *SoftwareEncoder) BuildCommand(config *EncodeConfig) *exec.Cmd {
|
|
// Use HDR pixel formats for EXR, SDR for PNG
|
|
var pixFmt string
|
|
var colorPrimaries, colorTrc, colorspace string
|
|
if config.SourceFormat == "png" {
|
|
// PNG: SDR format
|
|
pixFmt = "yuv420p"
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p"
|
|
}
|
|
colorPrimaries = "bt709"
|
|
colorTrc = "bt709"
|
|
colorspace = "bt709"
|
|
} else {
|
|
// EXR: Use HDR encoding if PreserveHDR is true, otherwise SDR (like PNG)
|
|
if config.PreserveHDR {
|
|
// HDR: Use HLG transfer with bt709 primaries to preserve HDR range while matching PNG color
|
|
pixFmt = "yuv420p10le" // 10-bit to preserve HDR range
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p10le"
|
|
}
|
|
colorPrimaries = "bt709" // bt709 primaries to match PNG color appearance
|
|
colorTrc = "arib-std-b67" // HLG transfer function - preserves HDR range, works on SDR displays
|
|
colorspace = "bt709" // bt709 colorspace to match PNG
|
|
} else {
|
|
// SDR: Treat as SDR (like PNG) - encode as bt709
|
|
pixFmt = "yuv420p"
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p"
|
|
}
|
|
colorPrimaries = "bt709"
|
|
colorTrc = "bt709"
|
|
colorspace = "bt709"
|
|
}
|
|
}
|
|
|
|
var codecArgs []string
|
|
switch e.codec {
|
|
case "libaom-av1":
|
|
codecArgs = []string{"-crf", strconv.Itoa(CRFAV1), "-b:v", "0", "-tiles", "2x2", "-g", "240"}
|
|
case "libvpx-vp9":
|
|
// VP9 supports alpha and HDR, use good quality settings
|
|
codecArgs = []string{"-crf", strconv.Itoa(CRFVP9), "-b:v", "0", "-row-mt", "1", "-g", "240"}
|
|
default:
|
|
// H.264: Use High 10 profile for HDR EXR (10-bit), High profile for SDR
|
|
if config.SourceFormat != "png" && config.PreserveHDR {
|
|
codecArgs = []string{"-preset", "veryslow", "-crf", strconv.Itoa(CRFH264), "-profile:v", "high10", "-level", "5.2", "-tune", "film", "-keyint_min", "24", "-g", "240", "-bf", "2", "-refs", "4"}
|
|
} else {
|
|
codecArgs = []string{"-preset", "veryslow", "-crf", strconv.Itoa(CRFH264), "-profile:v", "high", "-level", "5.2", "-tune", "film", "-keyint_min", "24", "-g", "240", "-bf", "2", "-refs", "4"}
|
|
}
|
|
}
|
|
|
|
args := []string{
|
|
"-y",
|
|
"-f", "image2",
|
|
"-start_number", fmt.Sprintf("%d", config.StartFrame),
|
|
"-framerate", fmt.Sprintf("%.2f", config.FrameRate),
|
|
"-i", config.InputPattern,
|
|
"-c:v", e.codec,
|
|
"-pix_fmt", pixFmt,
|
|
"-r", fmt.Sprintf("%.2f", config.FrameRate),
|
|
"-color_primaries", colorPrimaries,
|
|
"-color_trc", colorTrc,
|
|
"-colorspace", colorspace,
|
|
"-color_range", "tv",
|
|
}
|
|
|
|
// Add video filter for EXR: convert linear RGB based on HDR setting
|
|
// PNG doesn't need any filter as it's already in sRGB
|
|
if config.SourceFormat != "png" {
|
|
var vf string
|
|
if config.PreserveHDR {
|
|
// HDR: Convert linear RGB -> sRGB -> HLG with bt709 primaries
|
|
// This preserves HDR range while matching PNG color appearance
|
|
vf = "format=gbrpf32le,zscale=transferin=8:transfer=13:primariesin=1:primaries=1:matrixin=0:matrix=1:rangein=full:range=full,zscale=transferin=13:transfer=18:primariesin=1:primaries=1:matrixin=1:matrix=1:rangein=full:range=full"
|
|
if config.UseAlpha {
|
|
vf += ",format=yuva420p10le"
|
|
} else {
|
|
vf += ",format=yuv420p10le"
|
|
}
|
|
} else {
|
|
// SDR: Convert linear RGB (EXR) to sRGB (bt709) - simple conversion like Krita does
|
|
// zscale: linear (8) -> sRGB (13) with bt709 primaries/matrix
|
|
vf = "format=gbrpf32le,zscale=transferin=8:transfer=13:primariesin=1:primaries=1:matrixin=0:matrix=1:rangein=full:range=full"
|
|
if config.UseAlpha {
|
|
vf += ",format=yuva420p"
|
|
} else {
|
|
vf += ",format=yuv420p"
|
|
}
|
|
}
|
|
args = append(args, "-vf", vf)
|
|
}
|
|
args = append(args, codecArgs...)
|
|
|
|
if config.TwoPass {
|
|
// For 2-pass, this builds pass 2 command
|
|
args = append(args, "-pass", "2")
|
|
}
|
|
|
|
args = append(args, config.OutputPath)
|
|
|
|
if config.TwoPass {
|
|
log.Printf("Build Software Pass 2 command: ffmpeg %s", strings.Join(args, " "))
|
|
} else {
|
|
log.Printf("Build Software command: ffmpeg %s", strings.Join(args, " "))
|
|
}
|
|
cmd := exec.Command("ffmpeg", args...)
|
|
cmd.Dir = config.WorkDir
|
|
return cmd
|
|
}
|
|
|
|
// BuildPass1Command builds the first pass command for 2-pass encoding.
|
|
func (e *SoftwareEncoder) BuildPass1Command(config *EncodeConfig) *exec.Cmd {
|
|
// Use HDR pixel formats for EXR, SDR for PNG
|
|
var pixFmt string
|
|
var colorPrimaries, colorTrc, colorspace string
|
|
if config.SourceFormat == "png" {
|
|
// PNG: SDR format
|
|
pixFmt = "yuv420p"
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p"
|
|
}
|
|
colorPrimaries = "bt709"
|
|
colorTrc = "bt709"
|
|
colorspace = "bt709"
|
|
} else {
|
|
// EXR: Use HDR encoding if PreserveHDR is true, otherwise SDR (like PNG)
|
|
if config.PreserveHDR {
|
|
// HDR: Use HLG transfer with bt709 primaries to preserve HDR range while matching PNG color
|
|
pixFmt = "yuv420p10le" // 10-bit to preserve HDR range
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p10le"
|
|
}
|
|
colorPrimaries = "bt709" // bt709 primaries to match PNG color appearance
|
|
colorTrc = "arib-std-b67" // HLG transfer function - preserves HDR range, works on SDR displays
|
|
colorspace = "bt709" // bt709 colorspace to match PNG
|
|
} else {
|
|
// SDR: Treat as SDR (like PNG) - encode as bt709
|
|
pixFmt = "yuv420p"
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p"
|
|
}
|
|
colorPrimaries = "bt709"
|
|
colorTrc = "bt709"
|
|
colorspace = "bt709"
|
|
}
|
|
}
|
|
|
|
var codecArgs []string
|
|
switch e.codec {
|
|
case "libaom-av1":
|
|
codecArgs = []string{"-crf", strconv.Itoa(CRFAV1), "-b:v", "0", "-tiles", "2x2", "-g", "240"}
|
|
case "libvpx-vp9":
|
|
// VP9 supports alpha and HDR, use good quality settings
|
|
codecArgs = []string{"-crf", strconv.Itoa(CRFVP9), "-b:v", "0", "-row-mt", "1", "-g", "240"}
|
|
default:
|
|
// H.264: Use High 10 profile for HDR EXR (10-bit), High profile for SDR
|
|
if config.SourceFormat != "png" && config.PreserveHDR {
|
|
codecArgs = []string{"-preset", "veryslow", "-crf", strconv.Itoa(CRFH264), "-profile:v", "high10", "-level", "5.2", "-tune", "film", "-keyint_min", "24", "-g", "240", "-bf", "2", "-refs", "4"}
|
|
} else {
|
|
codecArgs = []string{"-preset", "veryslow", "-crf", strconv.Itoa(CRFH264), "-profile:v", "high", "-level", "5.2", "-tune", "film", "-keyint_min", "24", "-g", "240", "-bf", "2", "-refs", "4"}
|
|
}
|
|
}
|
|
|
|
args := []string{
|
|
"-y",
|
|
"-f", "image2",
|
|
"-start_number", fmt.Sprintf("%d", config.StartFrame),
|
|
"-framerate", fmt.Sprintf("%.2f", config.FrameRate),
|
|
"-i", config.InputPattern,
|
|
"-c:v", e.codec,
|
|
"-pix_fmt", pixFmt,
|
|
"-r", fmt.Sprintf("%.2f", config.FrameRate),
|
|
"-color_primaries", colorPrimaries,
|
|
"-color_trc", colorTrc,
|
|
"-colorspace", colorspace,
|
|
"-color_range", "tv",
|
|
}
|
|
|
|
// Add video filter for EXR: convert linear RGB based on HDR setting
|
|
// PNG doesn't need any filter as it's already in sRGB
|
|
if config.SourceFormat != "png" {
|
|
var vf string
|
|
if config.PreserveHDR {
|
|
// HDR: Convert linear RGB -> sRGB -> HLG with bt709 primaries
|
|
// This preserves HDR range while matching PNG color appearance
|
|
vf = "format=gbrpf32le,zscale=transferin=8:transfer=13:primariesin=1:primaries=1:matrixin=0:matrix=1:rangein=full:range=full,zscale=transferin=13:transfer=18:primariesin=1:primaries=1:matrixin=1:matrix=1:rangein=full:range=full"
|
|
if config.UseAlpha {
|
|
vf += ",format=yuva420p10le"
|
|
} else {
|
|
vf += ",format=yuv420p10le"
|
|
}
|
|
} else {
|
|
// SDR: Convert linear RGB (EXR) to sRGB (bt709) - simple conversion like Krita does
|
|
// zscale: linear (8) -> sRGB (13) with bt709 primaries/matrix
|
|
vf = "format=gbrpf32le,zscale=transferin=8:transfer=13:primariesin=1:primaries=1:matrixin=0:matrix=1:rangein=full:range=full"
|
|
if config.UseAlpha {
|
|
vf += ",format=yuva420p"
|
|
} else {
|
|
vf += ",format=yuv420p"
|
|
}
|
|
}
|
|
args = append(args, "-vf", vf)
|
|
}
|
|
|
|
args = append(args, codecArgs...)
|
|
args = append(args, "-pass", "1", "-f", "null", "/dev/null")
|
|
|
|
log.Printf("Build Software Pass 1 command: ffmpeg %s", strings.Join(args, " "))
|
|
cmd := exec.Command("ffmpeg", args...)
|
|
cmd.Dir = config.WorkDir
|
|
return cmd
|
|
}
|