main
Raw Download raw file
  1package pipeline
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log/slog"
  7	"time"
  8
  9	"github.com/crash/upvs/internal/ffmpeg"
 10	"github.com/crash/upvs/internal/manifest"
 11	"github.com/crash/upvs/internal/output"
 12)
 13
 14// RenderConfig holds configuration for the render phase.
 15type RenderConfig struct {
 16	TargetSecs   int
 17	FPS          int
 18	CRF          int
 19	Preset       string
 20	MinSpeedMult float64
 21	MaxSpeedMult float64
 22}
 23
 24// DefaultRenderConfig returns default render configuration.
 25func DefaultRenderConfig() RenderConfig {
 26	return RenderConfig{
 27		TargetSecs:   600,
 28		FPS:          30,
 29		CRF:          23,
 30		Preset:       "medium",
 31		MinSpeedMult: 1.0,
 32		MaxSpeedMult: 2000.0,
 33	}
 34}
 35
 36// RenderResult holds the output of the render phase.
 37type RenderResult struct {
 38	OutputPath     string
 39	SpeedFactor    float64
 40	OutputDuration float64
 41	Metadata       *manifest.DayMetadata
 42}
 43
 44// Render executes FFmpeg to create the timelapse.
 45func Render(ctx context.Context, layout *output.Layout, cfg RenderConfig) (*RenderResult, error) {
 46	// Build manifest first
 47	manifestResult, err := BuildManifest(ctx, layout)
 48	if err != nil {
 49		return nil, err
 50	}
 51
 52	// Calculate speed
 53	speedCfg := SpeedConfig{
 54		TargetSecs:   cfg.TargetSecs,
 55		MinSpeedMult: cfg.MinSpeedMult,
 56		MaxSpeedMult: cfg.MaxSpeedMult,
 57	}
 58	speedFactor, outputDuration := CalculateSpeed(manifestResult.TotalDuration, speedCfg)
 59
 60	slog.Info("starting render",
 61		slog.Float64("speed_factor", speedFactor),
 62		slog.Float64("expected_duration", outputDuration),
 63		slog.String("output", layout.TimelapsePath()))
 64
 65	// Run FFmpeg
 66	ffmpegCfg := ffmpeg.RenderConfig{
 67		ConcatFile:  layout.ConcatTxtPath(),
 68		OutputPath:  layout.TimelapsePath(),
 69		SpeedFactor: speedFactor,
 70		FPS:         cfg.FPS,
 71		CRF:         cfg.CRF,
 72		Preset:      cfg.Preset,
 73	}
 74
 75	err = ffmpeg.Render(ctx, ffmpegCfg)
 76	if err != nil {
 77		return nil, fmt.Errorf("rendering timelapse: %w", err)
 78	}
 79
 80	// Read clip index to get camera info
 81	clipIndex, err := manifest.ReadClipIndex(layout.ClipIndexPath())
 82	if err != nil {
 83		return nil, fmt.Errorf("reading clip index: %w", err)
 84	}
 85
 86	// Save day metadata
 87	dayMeta := &manifest.DayMetadata{
 88		CameraID:       clipIndex.CameraID,
 89		CameraName:     clipIndex.CameraName,
 90		Date:           clipIndex.Date,
 91		TotalClips:     len(clipIndex.Clips),
 92		ValidClips:     manifestResult.ValidClips,
 93		TotalDuration:  manifestResult.TotalDuration,
 94		SpeedFactor:    speedFactor,
 95		TargetSecs:     cfg.TargetSecs,
 96		OutputDuration: outputDuration,
 97		CreatedAt:      time.Now(),
 98	}
 99
100	err = manifest.WriteDayMetadata(layout.DayJSONPath(), dayMeta)
101	if err != nil {
102		return nil, err
103	}
104
105	slog.Info("render complete",
106		slog.String("output", layout.TimelapsePath()))
107
108	return &RenderResult{
109		OutputPath:     layout.TimelapsePath(),
110		SpeedFactor:    speedFactor,
111		OutputDuration: outputDuration,
112		Metadata:       dayMeta,
113	}, nil
114}