1a69fcfd04
- Updated the installation script to clarify that production wrappers do not include fixed test secrets, improving user guidance for local testing. - Modified the jiggablend-manager and jiggablend-runner scripts to remove fixed test configurations, emphasizing the use of local test credentials via `make init-test`. - Enhanced error handling in the runner script to ensure that an API key is provided, improving security and user feedback. - Added new flags and options for the runner, including sandboxing capabilities and GPU ray tracing control, enhancing flexibility for users. - Improved README documentation to reflect changes in usage and configuration, ensuring users have clear instructions for setup and execution.
595 lines
18 KiB
Go
595 lines
18 KiB
Go
package tasks
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"jiggablend/internal/runner/encoding"
|
|
)
|
|
|
|
// EncodeProcessor handles encode tasks.
|
|
type EncodeProcessor struct{}
|
|
|
|
// NewEncodeProcessor creates a new encode processor.
|
|
func NewEncodeProcessor() *EncodeProcessor {
|
|
return &EncodeProcessor{}
|
|
}
|
|
|
|
// Process executes an encode task.
|
|
func (p *EncodeProcessor) Process(ctx *Context) error {
|
|
if err := ctx.CheckCancelled(); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx.Info(fmt.Sprintf("Starting encode task: job %d", ctx.JobID))
|
|
log.Printf("Processing encode task %d for job %d", ctx.TaskID, ctx.JobID)
|
|
|
|
// Create temporary work directory
|
|
workDir, err := ctx.Workspace.CreateVideoDir(ctx.JobID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create work directory: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := ctx.Workspace.CleanupVideoDir(ctx.JobID); err != nil {
|
|
log.Printf("Warning: Failed to cleanup encode work directory: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Get output format and frame rate
|
|
outputFormat := ctx.GetOutputFormat()
|
|
if outputFormat == "" {
|
|
outputFormat = "EXR_264_MP4"
|
|
}
|
|
frameRate := ctx.GetFrameRate()
|
|
|
|
ctx.Info(fmt.Sprintf("Encode: detected output format '%s'", outputFormat))
|
|
ctx.Info(fmt.Sprintf("Encode: using frame rate %.2f fps", frameRate))
|
|
|
|
// Get job files
|
|
files, err := ctx.Manager.GetJobFiles(ctx.JobID)
|
|
if err != nil {
|
|
ctx.Error(fmt.Sprintf("Failed to get job files: %v", err))
|
|
return fmt.Errorf("failed to get job files: %w", err)
|
|
}
|
|
|
|
ctx.Info(fmt.Sprintf("GetJobFiles returned %d total files for job %d", len(files), ctx.JobID))
|
|
|
|
// Log all files for debugging
|
|
for _, file := range files {
|
|
ctx.Info(fmt.Sprintf("File: %s (type: %s, size: %d)", file.FileName, file.FileType, file.FileSize))
|
|
}
|
|
|
|
// Encode from EXR frames only
|
|
fileExt := ".exr"
|
|
frameFileSet := make(map[string]bool)
|
|
var frameFilesList []string
|
|
for _, file := range files {
|
|
if file.FileType == "output" && strings.HasSuffix(strings.ToLower(file.FileName), fileExt) {
|
|
if !frameFileSet[file.FileName] {
|
|
frameFileSet[file.FileName] = true
|
|
frameFilesList = append(frameFilesList, file.FileName)
|
|
}
|
|
}
|
|
}
|
|
if len(frameFilesList) == 0 {
|
|
// Log why no files matched (deduplicate for error reporting)
|
|
outputFileSet := make(map[string]bool)
|
|
frameFilesOtherTypeSet := make(map[string]bool)
|
|
var outputFiles []string
|
|
var frameFilesOtherType []string
|
|
|
|
for _, file := range files {
|
|
if file.FileType == "output" {
|
|
if !outputFileSet[file.FileName] {
|
|
outputFileSet[file.FileName] = true
|
|
outputFiles = append(outputFiles, file.FileName)
|
|
}
|
|
}
|
|
if strings.HasSuffix(strings.ToLower(file.FileName), fileExt) {
|
|
key := fmt.Sprintf("%s (type: %s)", file.FileName, file.FileType)
|
|
if !frameFilesOtherTypeSet[key] {
|
|
frameFilesOtherTypeSet[key] = true
|
|
frameFilesOtherType = append(frameFilesOtherType, key)
|
|
}
|
|
}
|
|
}
|
|
ctx.Error(fmt.Sprintf("no EXR frame files found for encode: found %d total files, %d unique output files, %d unique EXR files (with other types)", len(files), len(outputFiles), len(frameFilesOtherType)))
|
|
if len(outputFiles) > 0 {
|
|
ctx.Error(fmt.Sprintf("Output files found: %v", outputFiles))
|
|
}
|
|
if len(frameFilesOtherType) > 0 {
|
|
ctx.Error(fmt.Sprintf("EXR files with wrong type: %v", frameFilesOtherType))
|
|
}
|
|
err := fmt.Errorf("no EXR frame files found for encode")
|
|
return err
|
|
}
|
|
|
|
ctx.Info(fmt.Sprintf("Found %d EXR frames for encode", len(frameFilesList)))
|
|
|
|
// Download frames with bounded parallelism (8 concurrent downloads)
|
|
const downloadWorkers = 8
|
|
ctx.Info(fmt.Sprintf("Downloading %d EXR frames for encode...", len(frameFilesList)))
|
|
|
|
type result struct {
|
|
path string
|
|
err error
|
|
}
|
|
results := make([]result, len(frameFilesList))
|
|
var wg sync.WaitGroup
|
|
sem := make(chan struct{}, downloadWorkers)
|
|
for i, fileName := range frameFilesList {
|
|
wg.Add(1)
|
|
go func(i int, fileName string) {
|
|
defer wg.Done()
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
framePath := filepath.Join(workDir, fileName)
|
|
err := ctx.Manager.DownloadFrame(ctx.JobID, fileName, framePath)
|
|
if err != nil {
|
|
ctx.Error(fmt.Sprintf("Failed to download EXR frame %s: %v", fileName, err))
|
|
log.Printf("Failed to download EXR frame for encode %s: %v", fileName, err)
|
|
results[i] = result{"", err}
|
|
return
|
|
}
|
|
results[i] = result{framePath, nil}
|
|
}(i, fileName)
|
|
}
|
|
wg.Wait()
|
|
|
|
var frameFiles []string
|
|
for _, r := range results {
|
|
if r.err == nil && r.path != "" {
|
|
frameFiles = append(frameFiles, r.path)
|
|
}
|
|
}
|
|
if err := ctx.CheckCancelled(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(frameFiles) == 0 {
|
|
err := fmt.Errorf("failed to download any EXR frames for encode")
|
|
ctx.Error(err.Error())
|
|
return err
|
|
}
|
|
|
|
sort.Strings(frameFiles)
|
|
ctx.Info(fmt.Sprintf("Downloaded %d frames", len(frameFiles)))
|
|
|
|
// Check if EXR files have alpha channel (for encode decision)
|
|
hasAlpha := false
|
|
{
|
|
firstFrame := frameFiles[0]
|
|
hasAlpha = detectAlphaChannel(ctx, firstFrame)
|
|
if hasAlpha {
|
|
ctx.Info("Detected alpha channel in EXR files")
|
|
} else {
|
|
ctx.Info("No alpha channel detected in EXR files")
|
|
}
|
|
}
|
|
|
|
// Generate video
|
|
// Use alpha when source EXR has alpha and codec supports it (AV1 or VP9). H.264 does not support alpha.
|
|
useAlpha := hasAlpha && (outputFormat == "EXR_AV1_MP4" || outputFormat == "EXR_VP9_WEBM")
|
|
if hasAlpha && outputFormat == "EXR_264_MP4" {
|
|
ctx.Warn("Alpha channel detected in EXR but H.264 does not support alpha. Use EXR_AV1_MP4 or EXR_VP9_WEBM to preserve alpha in video.")
|
|
}
|
|
if useAlpha {
|
|
ctx.Info("Alpha channel detected - encoding with alpha (AV1/VP9)")
|
|
}
|
|
var outputExt string
|
|
switch outputFormat {
|
|
case "EXR_VP9_WEBM":
|
|
outputExt = "webm"
|
|
ctx.Info("Encoding WebM video with VP9 codec (alpha, HDR)...")
|
|
case "EXR_AV1_MP4":
|
|
outputExt = "mp4"
|
|
ctx.Info("Encoding MP4 video with AV1 codec (alpha, HDR)...")
|
|
default:
|
|
outputExt = "mp4"
|
|
ctx.Info("Encoding MP4 video with H.264 codec (HDR, HLG)...")
|
|
}
|
|
|
|
outputVideo := filepath.Join(workDir, fmt.Sprintf("output_%d.%s", ctx.JobID, outputExt))
|
|
|
|
// Build input pattern
|
|
firstFrame := frameFiles[0]
|
|
baseName := filepath.Base(firstFrame)
|
|
re := regexp.MustCompile(`_(\d+)\.`)
|
|
var pattern string
|
|
var startNumber int
|
|
frameNumStr := re.FindStringSubmatch(baseName)
|
|
if len(frameNumStr) > 1 {
|
|
pattern = re.ReplaceAllString(baseName, "_%04d.")
|
|
fmt.Sscanf(frameNumStr[1], "%d", &startNumber)
|
|
} else {
|
|
startNumber = extractFrameNumber(baseName)
|
|
pattern = strings.Replace(baseName, fmt.Sprintf("%d", startNumber), "%04d", 1)
|
|
}
|
|
patternPath := filepath.Join(workDir, pattern)
|
|
|
|
// Select encoder and build command (software encoding only)
|
|
var encoder encoding.Encoder
|
|
switch outputFormat {
|
|
case "EXR_AV1_MP4":
|
|
encoder = ctx.Encoder.SelectAV1()
|
|
case "EXR_VP9_WEBM":
|
|
encoder = ctx.Encoder.SelectVP9()
|
|
default:
|
|
encoder = ctx.Encoder.SelectH264()
|
|
}
|
|
|
|
ctx.Info(fmt.Sprintf("Using encoder: %s (%s)", encoder.Name(), encoder.Codec()))
|
|
|
|
// All software encoders use 2-pass for optimal quality
|
|
ctx.Info("Starting 2-pass encode for optimal quality...")
|
|
|
|
// Pass 1
|
|
ctx.Info("Pass 1/2: Analyzing content for optimal encode...")
|
|
softEncoder := encoder.(*encoding.SoftwareEncoder)
|
|
pass1Cmd := softEncoder.BuildPass1Command(&encoding.EncodeConfig{
|
|
InputPattern: patternPath,
|
|
OutputPath: outputVideo,
|
|
StartFrame: startNumber,
|
|
FrameRate: frameRate,
|
|
WorkDir: workDir,
|
|
UseAlpha: useAlpha,
|
|
TwoPass: true,
|
|
})
|
|
if err := pass1Cmd.Run(); err != nil {
|
|
// Pass 1 is analysis-only (writes to /dev/null). FFmpeg often exits non-zero
|
|
// on benign codec/option warnings while still producing passlogfile stats.
|
|
ctx.Warn(fmt.Sprintf("Pass 1 completed (warnings expected): %v", err))
|
|
}
|
|
|
|
// Pass 2
|
|
ctx.Info("Pass 2/2: Encoding with optimal quality...")
|
|
|
|
config := &encoding.EncodeConfig{
|
|
InputPattern: patternPath,
|
|
OutputPath: outputVideo,
|
|
StartFrame: startNumber,
|
|
FrameRate: frameRate,
|
|
WorkDir: workDir,
|
|
UseAlpha: useAlpha,
|
|
TwoPass: true, // Software encoding always uses 2-pass for quality
|
|
}
|
|
|
|
cmd := encoder.BuildCommand(config)
|
|
if cmd == nil {
|
|
return errors.New("failed to build encode command")
|
|
}
|
|
|
|
// Set up pipes
|
|
stdoutPipe, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
|
}
|
|
|
|
stderrPipe, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start encode command: %w", err)
|
|
}
|
|
stopMonitor := ctx.StartCancellationMonitor(cmd, "encode")
|
|
defer stopMonitor()
|
|
|
|
ctx.Processes.Track(ctx.TaskID, cmd)
|
|
defer ctx.Processes.Untrack(ctx.TaskID)
|
|
|
|
// Stream stdout
|
|
stdoutDone := make(chan bool)
|
|
go func() {
|
|
defer close(stdoutDone)
|
|
scanner := bufio.NewScanner(stdoutPipe)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if line != "" {
|
|
ctx.Info(line)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
log.Printf("Error reading encode stdout: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Stream stderr
|
|
stderrDone := make(chan bool)
|
|
go func() {
|
|
defer close(stderrDone)
|
|
scanner := bufio.NewScanner(stderrPipe)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if line != "" {
|
|
ctx.Warn(line)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
log.Printf("Error reading encode stderr: %v", err)
|
|
}
|
|
}()
|
|
|
|
err = cmd.Wait()
|
|
<-stdoutDone
|
|
<-stderrDone
|
|
|
|
if err != nil {
|
|
if cancelled, checkErr := ctx.IsJobCancelled(); checkErr == nil && cancelled {
|
|
return ErrJobCancelled
|
|
}
|
|
var errMsg string
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
if exitErr.ExitCode() == 137 {
|
|
errMsg = "FFmpeg was killed due to excessive memory usage (OOM)"
|
|
} else {
|
|
errMsg = fmt.Sprintf("ffmpeg encoding failed: %v", err)
|
|
}
|
|
} else {
|
|
errMsg = fmt.Sprintf("ffmpeg encoding failed: %v", err)
|
|
}
|
|
|
|
if sizeErr := checkFFmpegSizeError(errMsg); sizeErr != nil {
|
|
ctx.Error(sizeErr.Error())
|
|
return sizeErr
|
|
}
|
|
|
|
ctx.Error(errMsg)
|
|
return errors.New(errMsg)
|
|
}
|
|
|
|
// Verify output
|
|
if _, err := os.Stat(outputVideo); os.IsNotExist(err) {
|
|
err := fmt.Errorf("video %s file not created: %s", outputExt, outputVideo)
|
|
ctx.Error(err.Error())
|
|
return err
|
|
}
|
|
|
|
// Clean up 2-pass log files
|
|
os.Remove(filepath.Join(workDir, "ffmpeg2pass-0.log"))
|
|
os.Remove(filepath.Join(workDir, "ffmpeg2pass-0.log.mbtree"))
|
|
|
|
ctx.Info(fmt.Sprintf("%s video encoded successfully", strings.ToUpper(outputExt)))
|
|
|
|
// Upload video
|
|
ctx.Info(fmt.Sprintf("Uploading encoded %s video...", strings.ToUpper(outputExt)))
|
|
|
|
uploadPath := fmt.Sprintf("/api/runner/jobs/%d/upload", ctx.JobID)
|
|
if err := ctx.Manager.UploadFile(uploadPath, ctx.JobToken, outputVideo); err != nil {
|
|
ctx.Error(fmt.Sprintf("Failed to upload %s: %v", strings.ToUpper(outputExt), err))
|
|
return fmt.Errorf("failed to upload %s: %w", strings.ToUpper(outputExt), err)
|
|
}
|
|
|
|
ctx.Info(fmt.Sprintf("Successfully uploaded %s: %s", strings.ToUpper(outputExt), filepath.Base(outputVideo)))
|
|
|
|
// Delete file after successful upload to prevent duplicate uploads
|
|
if err := os.Remove(outputVideo); err != nil {
|
|
log.Printf("Warning: Failed to delete video file %s after upload: %v", outputVideo, err)
|
|
ctx.Warn(fmt.Sprintf("Warning: Failed to delete video file after upload: %v", err))
|
|
}
|
|
|
|
log.Printf("Successfully generated and uploaded %s for job %d: %s", strings.ToUpper(outputExt), ctx.JobID, filepath.Base(outputVideo))
|
|
return nil
|
|
}
|
|
|
|
// detectAlphaChannel checks if an EXR file has an alpha channel using ffprobe
|
|
func detectAlphaChannel(ctx *Context, filePath string) bool {
|
|
// Use ffprobe to check pixel format and stream properties
|
|
// EXR files with alpha will have formats like gbrapf32le (RGBA) vs gbrpf32le (RGB)
|
|
cmd := execCommand("ffprobe",
|
|
"-v", "error",
|
|
"-select_streams", "v:0",
|
|
"-show_entries", "stream=pix_fmt:stream=codec_name",
|
|
"-of", "default=noprint_wrappers=1",
|
|
filePath,
|
|
)
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
// If ffprobe fails, assume no alpha (conservative approach)
|
|
ctx.Warn(fmt.Sprintf("Failed to detect alpha channel in %s: %v", filepath.Base(filePath), err))
|
|
return false
|
|
}
|
|
|
|
outputStr := string(output)
|
|
// Check pixel format - EXR with alpha typically has 'a' in the format name (e.g., gbrapf32le)
|
|
// Also check for formats that explicitly indicate alpha
|
|
hasAlpha := strings.Contains(outputStr, "pix_fmt=gbrap") ||
|
|
strings.Contains(outputStr, "pix_fmt=rgba") ||
|
|
strings.Contains(outputStr, "pix_fmt=yuva") ||
|
|
strings.Contains(outputStr, "pix_fmt=abgr")
|
|
|
|
if hasAlpha {
|
|
ctx.Info(fmt.Sprintf("Detected alpha channel in EXR file: %s", filepath.Base(filePath)))
|
|
}
|
|
|
|
return hasAlpha
|
|
}
|
|
|
|
// detectHDR checks if an EXR file contains HDR content using ffprobe
|
|
func detectHDR(ctx *Context, filePath string) bool {
|
|
// First, check if the pixel format supports HDR (32-bit float)
|
|
cmd := execCommand("ffprobe",
|
|
"-v", "error",
|
|
"-select_streams", "v:0",
|
|
"-show_entries", "stream=pix_fmt",
|
|
"-of", "default=noprint_wrappers=1:nokey=1",
|
|
filePath,
|
|
)
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
// If ffprobe fails, assume no HDR (conservative approach)
|
|
ctx.Warn(fmt.Sprintf("Failed to detect HDR in %s: %v", filepath.Base(filePath), err))
|
|
return false
|
|
}
|
|
|
|
pixFmt := strings.TrimSpace(string(output))
|
|
// EXR files with 32-bit float format (gbrpf32le, gbrapf32le) can contain HDR
|
|
// Check if it's a 32-bit float format
|
|
isFloat32 := strings.Contains(pixFmt, "f32") || strings.Contains(pixFmt, "f32le")
|
|
|
|
if !isFloat32 {
|
|
// Not a float format, definitely not HDR
|
|
return false
|
|
}
|
|
|
|
// For 32-bit float EXR, sample pixels to check if values exceed SDR range (> 1.0)
|
|
// Use ffmpeg to extract pixel statistics - check max pixel values
|
|
// This is more efficient than sampling individual pixels
|
|
cmd = execCommand("ffmpeg",
|
|
"-v", "error",
|
|
"-i", filePath,
|
|
"-vf", "signalstats",
|
|
"-f", "null",
|
|
"-",
|
|
)
|
|
|
|
output, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
// If stats extraction fails, try sampling a few pixels directly
|
|
return detectHDRBySampling(ctx, filePath)
|
|
}
|
|
|
|
// Check output for max pixel values
|
|
outputStr := string(output)
|
|
// Look for max values in the signalstats output
|
|
// If we find values > 1.0, it's HDR
|
|
if strings.Contains(outputStr, "MAX") {
|
|
// Try to extract max values from signalstats output
|
|
// Format is typically like: YMAX:1.234 UMAX:0.567 VMAX:0.890
|
|
// For EXR (RGB), we need to check R, G, B channels
|
|
// Since signalstats works on YUV, we'll use a different approach
|
|
return detectHDRBySampling(ctx, filePath)
|
|
}
|
|
|
|
// Fallback to pixel sampling
|
|
return detectHDRBySampling(ctx, filePath)
|
|
}
|
|
|
|
// detectHDRBySampling samples pixels from multiple regions to detect HDR content
|
|
func detectHDRBySampling(ctx *Context, filePath string) bool {
|
|
// Sample multiple 10x10 regions from different parts of the image
|
|
// This gives us better coverage than a single sample
|
|
sampleRegions := []string{
|
|
"crop=10:10:iw/4:ih/4", // Top-left quadrant
|
|
"crop=10:10:iw*3/4:ih/4", // Top-right quadrant
|
|
"crop=10:10:iw/4:ih*3/4", // Bottom-left quadrant
|
|
"crop=10:10:iw*3/4:ih*3/4", // Bottom-right quadrant
|
|
"crop=10:10:iw/2:ih/2", // Center
|
|
}
|
|
|
|
for _, region := range sampleRegions {
|
|
cmd := execCommand("ffmpeg",
|
|
"-v", "error",
|
|
"-i", filePath,
|
|
"-vf", fmt.Sprintf("%s,scale=1:1", region),
|
|
"-f", "rawvideo",
|
|
"-pix_fmt", "gbrpf32le",
|
|
"-",
|
|
)
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
continue // Skip this region if sampling fails
|
|
}
|
|
|
|
// Parse the float32 values (4 bytes per float, 3 channels RGB)
|
|
if len(output) >= 12 { // At least 3 floats (RGB) = 12 bytes
|
|
for i := 0; i < len(output)-11; i += 12 {
|
|
// Read RGB values (little-endian float32)
|
|
r := float32FromBytes(output[i : i+4])
|
|
g := float32FromBytes(output[i+4 : i+8])
|
|
b := float32FromBytes(output[i+8 : i+12])
|
|
|
|
// Check if any channel exceeds 1.0 (SDR range)
|
|
if r > 1.0 || g > 1.0 || b > 1.0 {
|
|
maxVal := max(r, max(g, b))
|
|
ctx.Info(fmt.Sprintf("Detected HDR content in EXR file: %s (max value: %.2f)", filepath.Base(filePath), maxVal))
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we sampled multiple regions and none exceed 1.0, it's likely SDR content
|
|
// But since it's 32-bit float format, user can still manually enable HDR if needed
|
|
return false
|
|
}
|
|
|
|
// float32FromBytes converts 4 bytes (little-endian) to float32
|
|
func float32FromBytes(bytes []byte) float32 {
|
|
if len(bytes) < 4 {
|
|
return 0
|
|
}
|
|
bits := uint32(bytes[0]) | uint32(bytes[1])<<8 | uint32(bytes[2])<<16 | uint32(bytes[3])<<24
|
|
return math.Float32frombits(bits)
|
|
}
|
|
|
|
// max returns the maximum of two float32 values
|
|
func max(a, b float32) float32 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func extractFrameNumber(filename string) int {
|
|
parts := strings.Split(filepath.Base(filename), "_")
|
|
if len(parts) < 2 {
|
|
return 0
|
|
}
|
|
framePart := strings.Split(parts[1], ".")[0]
|
|
var frameNum int
|
|
fmt.Sscanf(framePart, "%d", &frameNum)
|
|
return frameNum
|
|
}
|
|
|
|
func checkFFmpegSizeError(output string) error {
|
|
outputLower := strings.ToLower(output)
|
|
|
|
if strings.Contains(outputLower, "hardware does not support encoding at size") {
|
|
constraintsMatch := regexp.MustCompile(`constraints:\s*width\s+(\d+)-(\d+)\s+height\s+(\d+)-(\d+)`).FindStringSubmatch(output)
|
|
if len(constraintsMatch) == 5 {
|
|
return fmt.Errorf("video frame size is outside hardware encoder limits. Hardware requires: width %s-%s, height %s-%s",
|
|
constraintsMatch[1], constraintsMatch[2], constraintsMatch[3], constraintsMatch[4])
|
|
}
|
|
return fmt.Errorf("video frame size is outside hardware encoder limits")
|
|
}
|
|
|
|
if strings.Contains(outputLower, "picture size") && strings.Contains(outputLower, "is invalid") {
|
|
sizeMatch := regexp.MustCompile(`picture size\s+(\d+)x(\d+)`).FindStringSubmatch(output)
|
|
if len(sizeMatch) == 3 {
|
|
return fmt.Errorf("invalid video frame size: %sx%s", sizeMatch[1], sizeMatch[2])
|
|
}
|
|
return fmt.Errorf("invalid video frame size")
|
|
}
|
|
|
|
if strings.Contains(outputLower, "error while opening encoder") &&
|
|
(strings.Contains(outputLower, "width") || strings.Contains(outputLower, "height") || strings.Contains(outputLower, "size")) {
|
|
sizeMatch := regexp.MustCompile(`at size\s+(\d+)x(\d+)`).FindStringSubmatch(output)
|
|
if len(sizeMatch) == 3 {
|
|
return fmt.Errorf("hardware encoder cannot encode frame size %sx%s", sizeMatch[1], sizeMatch[2])
|
|
}
|
|
return fmt.Errorf("hardware encoder error: frame size may be invalid")
|
|
}
|
|
|
|
if strings.Contains(outputLower, "invalid") &&
|
|
(strings.Contains(outputLower, "width") || strings.Contains(outputLower, "height") || strings.Contains(outputLower, "dimension")) {
|
|
return fmt.Errorf("invalid frame dimensions detected")
|
|
}
|
|
|
|
return nil
|
|
}
|