main
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}