- Removed Node.js build artifacts from .gitignore and adjusted Makefile to reflect changes in web UI build process, now using server-rendered Go templates instead of React. - Updated README to clarify the new web UI architecture and output formats, emphasizing the removal of the Node.js build step. - Added a command to set the number of frames per render task in manager configuration, enhancing user control over rendering settings. - Improved Gitea workflow by removing unnecessary npm install step, streamlining the CI process.
146 lines
6.3 KiB
Go
146 lines
6.3 KiB
Go
package encoding
|
|
|
|
// Pipeline: Blender outputs only EXR (linear). Encode is EXR only: linear -> sRGB -> HLG (video), 10-bit, full range.
|
|
|
|
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 {
|
|
// EXR only: HDR path (HLG, 10-bit, full range)
|
|
pixFmt := "yuv420p10le"
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p10le"
|
|
}
|
|
colorPrimaries, colorTrc, colorspace, colorRange := "bt709", "arib-std-b67", "bt709", "pc"
|
|
|
|
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":
|
|
codecArgs = []string{"-crf", strconv.Itoa(CRFVP9), "-b:v", "0", "-row-mt", "1", "-g", "240"}
|
|
default:
|
|
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"}
|
|
}
|
|
|
|
args := []string{"-y", "-f", "image2", "-start_number", fmt.Sprintf("%d", config.StartFrame), "-framerate", fmt.Sprintf("%.2f", config.FrameRate),
|
|
"-color_trc", "linear", "-color_primaries", "bt709"}
|
|
args = append(args, "-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", colorRange)
|
|
|
|
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"
|
|
}
|
|
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 {
|
|
pixFmt := "yuv420p10le"
|
|
if config.UseAlpha {
|
|
pixFmt = "yuva420p10le"
|
|
}
|
|
colorPrimaries, colorTrc, colorspace, colorRange := "bt709", "arib-std-b67", "bt709", "pc"
|
|
|
|
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":
|
|
codecArgs = []string{"-crf", strconv.Itoa(CRFVP9), "-b:v", "0", "-row-mt", "1", "-g", "240"}
|
|
default:
|
|
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"}
|
|
}
|
|
|
|
args := []string{"-y", "-f", "image2", "-start_number", fmt.Sprintf("%d", config.StartFrame), "-framerate", fmt.Sprintf("%.2f", config.FrameRate),
|
|
"-color_trc", "linear", "-color_primaries", "bt709"}
|
|
args = append(args, "-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", colorRange)
|
|
|
|
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"
|
|
}
|
|
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
|
|
}
|