70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
|
|
package runner
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"log"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"path/filepath"
|
||
|
|
"strconv"
|
||
|
|
)
|
||
|
|
|
||
|
|
// RunSnapshot renders a single frame of compName from aepPath and writes a PNG
|
||
|
|
// still, returning its path. It reuses the render pipeline shape: aerender emits
|
||
|
|
// the comp's output module (lossless AVI/MOV) for one frame, then ffmpeg extracts
|
||
|
|
// a single PNG. Requires aerender (aePath) and ffmpeg on the node.
|
||
|
|
func RunSnapshot(ctx context.Context, aePath, aepPath, compName string, frame int, workDir string) (string, error) {
|
||
|
|
if aePath == "" {
|
||
|
|
return "", fmt.Errorf("AE path required for snapshot render")
|
||
|
|
}
|
||
|
|
if compName == "" {
|
||
|
|
return "", fmt.Errorf("comp name required for snapshot render")
|
||
|
|
}
|
||
|
|
if err := os.MkdirAll(workDir, 0o755); err != nil {
|
||
|
|
return "", fmt.Errorf("workdir: %w", err)
|
||
|
|
}
|
||
|
|
out := filepath.Join(workDir, "snap.avi")
|
||
|
|
_ = os.Remove(out)
|
||
|
|
|
||
|
|
// -s/-e bound the render to a single frame; aerender writes via the comp's
|
||
|
|
// output module (cmd.Dir = project folder so relative footage resolves).
|
||
|
|
args := []string{
|
||
|
|
"-project", aepPath, "-comp", compName,
|
||
|
|
"-s", strconv.Itoa(frame), "-e", strconv.Itoa(frame),
|
||
|
|
"-output", out,
|
||
|
|
}
|
||
|
|
log.Printf("[snapshot] aerender %v", args)
|
||
|
|
cmd := exec.CommandContext(ctx, aePath, args...)
|
||
|
|
cmd.Dir = filepath.Dir(aepPath)
|
||
|
|
cmd.Stdout = os.Stdout
|
||
|
|
cmd.Stderr = os.Stderr
|
||
|
|
if err := cmd.Run(); err != nil {
|
||
|
|
return "", fmt.Errorf("aerender: %w", err)
|
||
|
|
}
|
||
|
|
actual := findRenderedOutput(out)
|
||
|
|
if actual == "" {
|
||
|
|
return "", fmt.Errorf("aerender produced no output for comp %q", compName)
|
||
|
|
}
|
||
|
|
|
||
|
|
ff := ffmpegPath()
|
||
|
|
if ff == "" {
|
||
|
|
return "", fmt.Errorf("ffmpeg not found (set FFMPEG_PATH or place ffmpeg.exe next to the agent)")
|
||
|
|
}
|
||
|
|
png := filepath.Join(workDir, "snap.png")
|
||
|
|
_ = os.Remove(png)
|
||
|
|
ffArgs := []string{"-y", "-i", actual, "-frames:v", "1", png}
|
||
|
|
log.Printf("[snapshot] ffmpeg %v", ffArgs)
|
||
|
|
fc := exec.CommandContext(ctx, ff, ffArgs...)
|
||
|
|
fc.Stdout = os.Stdout
|
||
|
|
fc.Stderr = os.Stderr
|
||
|
|
if err := fc.Run(); err != nil {
|
||
|
|
return "", fmt.Errorf("ffmpeg still: %w", err)
|
||
|
|
}
|
||
|
|
_ = os.Remove(actual) // drop the intermediate render
|
||
|
|
if st, err := os.Stat(png); err != nil || st.Size() == 0 {
|
||
|
|
return "", fmt.Errorf("no snapshot image produced")
|
||
|
|
}
|
||
|
|
return png, nil
|
||
|
|
}
|