timelapse

package
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: AGPL-3.0 Imports: 33 Imported by: 0

Documentation

Overview

Shared MP4 box-skeleton + frame I/O for the H.264 and H.265 Go mergers.

h264_go_merge.go and h265_go_merge.go each previously carried their own byte-identical copy of the moov/mvhd/trak/mdia/minf/stbl box chain (the only real divergence was the sample-entry box: avc1+avcC vs hvc1+hvcC). This file consolidates that skeleton into one codecMuxer type plus the shared frame helpers (splitAnnexB, bitReader, removeEmulationPrevention) that were already de-facto shared across files. See #236.

Package timelapse — Recording frame extraction (pure Go, no external deps).

RecordingFrameExtractor extracts frames from recording files at regular intervals for timelapse generation. Supports three formats:

  • AVI (MJPEG JPEG frames via internal/avi demuxer)
  • H264 MP4 (IDR sync samples via merge.ParseSegment + stss)
  • H265 MP4 (IRAP NAL type 19/20 sync samples)

Output frames are written to a temporary directory as:

  • frame_000001.jpg (AVI)
  • frame_000001.h264 (H264 MP4, Annex-B with SPS/PPS)
  • frame_000001.h265 (H265 MP4, Annex-B with VPS/SPS/PPS)

Memory constraint: uses io.ReadAt for seeking to sample offsets instead of loading the entire recording file. Verified <50MB RSS for 1hr H264.

Package timelapse — Pure Go H.264 NAL → MP4 muxer.

H264GoMerger converts raw H.264 keyframe files (Annex-B format with 0x00000001 start codes) captured by KeyframeExtractor into playable MP4 timelapse videos using only the abema/go-mp4 library and the Go standard library. No CGO, no external binaries.

Package timelapse — Pure Go H.265/HEVC NAL → MP4 muxer.

H265GoMerger converts raw H.265 keyframe files (Annex-B format with 0x00000001 start codes) captured by KeyframeExtractor into playable MP4 timelapse videos using only the abema/go-mp4 library and the Go standard library. No CGO, no external binaries.

Package timelapse provides keyframe extraction from active recorder StreamHubs.

Package timelapse provides merge scheduling for timelapse recordings.

Package timelapse provides rolling merge functionality for timelapse recordings.

Package timelapse provides types and interfaces for timelapse recording and segment merging.

Package timelapse provides types and interfaces for timelapse recording and segment merging.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DeriveSnapshotURL added in v0.7.0

func DeriveSnapshotURL(streamURL, protocol string) string

DeriveSnapshotURL attempts to derive an HTTP snapshot URL from a camera's stream URL and protocol. Returns the derived URL, or empty string if no derivation is possible (caller should fall back to other snapshot sources).

Derivation rules:

  • RTSP (rtsp://...): tries common HTTP snapshot endpoints
  • ONVIF: returns empty (callers should use GetSnapshotURI from ONVIF client)
  • HTTP (http://...): returns the URL as-is (already an HTTP endpoint)
  • Xiaomi: returns empty (no snapshot support)
  • Unknown protocol: returns empty

func IsMergeAvailable

func IsMergeAvailable() bool

IsMergeAvailable returns true if the cached merge tier is better than JPEG. Returns false if DetectMergeTier has not been called yet.

func ResetDetectTier

func ResetDetectTier()

ResetDetectTier clears the cached detection result.

func SnapshotCandidates added in v0.7.0

func SnapshotCandidates(streamURL, protocol string) []string

SnapshotCandidates returns all possible HTTP snapshot URLs derived from an RTSP URL, ordered by likelihood. Returns nil if no candidates can be derived.

Types

type AutoDetectMerger added in v0.7.0

type AutoDetectMerger struct {
	// contains filtered or unexported fields
}

AutoDetectMerger wraps multiple mergers and picks the right one based on frame file types in the directory. It checks for .h265 files first (uses H265GoMerger), then .h264 files (uses H264GoMerger), then falls back to GoMerger for JPEG frames.

func NewAutoDetectMerger added in v0.7.0

func NewAutoDetectMerger() *AutoDetectMerger

NewAutoDetectMerger creates a merger that auto-detects frame types.

func (*AutoDetectMerger) CanMerge added in v0.7.0

func (m *AutoDetectMerger) CanMerge() bool

func (*AutoDetectMerger) Merge added in v0.7.0

func (m *AutoDetectMerger) Merge(ctx context.Context, framesDir, outputPath string, fps int) (*MergeResult, error)

func (*AutoDetectMerger) Tier added in v0.7.0

func (m *AutoDetectMerger) Tier() MergeTier

type DailyMergeManager

type DailyMergeManager struct {
	// contains filtered or unexported fields
}

DailyMergeManager handles daily merge operations for timelapse recordings. It wraps PeriodicMergeManager with a 24-hour interval for backward compatibility.

func NewDailyMergeManager

func NewDailyMergeManager(store RecordingLister, updater MergeStatusUpdater, merger TimelapseMerger, fps int, dataDir string, loc *time.Location) *DailyMergeManager

NewDailyMergeManager creates a new DailyMergeManager wrapping a PeriodicMergeManager with 24h duration. If loc is nil, UTC is used.

func (*DailyMergeManager) Run

func (m *DailyMergeManager) Run(ctx context.Context, cameraID string, date string) error

Run executes the daily merge pipeline for the given camera on the given date. date format: "2006-01-02" (interpreted in the configured timezone)

type FFmpegMerger

type FFmpegMerger struct {
	// contains filtered or unexported fields
}

FFmpegMerger implements TimelapseMerger using FFmpeg for JPEG→MP4 transcoding. It supports hardware-accelerated encoding with software libx264 fallback, configurable CRF/bitrate, and automatic codec detection via ffprobe.

func NewFFmpegMerger

func NewFFmpegMerger(caps *transcoding.HardwareCapabilities, config *MergeConfig) *FFmpegMerger

NewFFmpegMerger creates a new FFmpegMerger with the given hardware capabilities and optional merge configuration.

func (*FFmpegMerger) CanMerge

func (m *FFmpegMerger) CanMerge() bool

CanMerge reports whether FFmpeg is available on this system.

func (*FFmpegMerger) Merge

func (m *FFmpegMerger) Merge(ctx context.Context, framesDir, outputPath string, fps int) (*MergeResult, error)

Merge performs the merge of JPEG frame files from framesDir into outputPath at the given fps. It implements the fallback chain:

  1. FFmpeg with hardware encoder (v4l2m2m/VAAPI)
  2. FFmpeg with software libx264

After a successful merge, it detects the output codec via ffprobe.

func (*FFmpegMerger) Tier

func (m *FFmpegMerger) Tier() MergeTier

Tier returns the merge tier identifier.

type FrameSource added in v0.7.0

type FrameSource string

FrameSource represents the source of frames for timelapse recording.

const (
	// FrameSourceAuto auto-detects the best frame source based on camera capabilities.
	FrameSourceAuto FrameSource = "auto"
	// FrameSourceSnapshot uses HTTP snapshot endpoint for frame capture.
	FrameSourceSnapshot FrameSource = "snapshot"
	// FrameSourceRTSPKeyframe extracts keyframes from RTSP stream for frame capture.
	FrameSourceRTSPKeyframe FrameSource = "rtsp_keyframe"
	// FrameSourceMJPEG uses MJPEG stream for frame capture.
	FrameSourceMJPEG FrameSource = "mjpeg"
)

type GoMerger

type GoMerger struct {
	// contains filtered or unexported fields
}

GoMerger implements TimelapseMerger using pure Go to create an MP4 file from JPEG frames. Each JPEG is stored as a sample in an MJPEG video track. When jpegQuality >= 0, frames are decoded and re-encoded at the given quality to reduce file size. When jpegQuality < 0, original JPEG data is used as-is (passthrough).

func NewEnhancedGoMerger added in v0.7.0

func NewEnhancedGoMerger(quality int) *GoMerger

NewEnhancedGoMerger creates a GoMerger that re-encodes JPEG frames at the given quality. quality range: 1-100 (1 = smallest files/poor quality, 100 = best quality/large files). Recommended archival quality: 30 (60-70% size reduction vs Q=85, visually acceptable).

func NewGoMerger

func NewGoMerger() *GoMerger

NewGoMerger creates a new GoMerger with passthrough mode (original JPEG quality preserved).

func (*GoMerger) CanMerge

func (m *GoMerger) CanMerge() bool

CanMerge always returns true since this is a pure Go implementation.

func (*GoMerger) Merge

func (m *GoMerger) Merge(ctx context.Context, framesDir, outputPath string, fps int) (*MergeResult, error)

func (*GoMerger) Tier

func (m *GoMerger) Tier() MergeTier

Tier returns the merge tier identifier.

type H264GoMerger added in v0.7.0

type H264GoMerger struct{}

H264GoMerger implements TimelapseMerger using pure Go to create an MP4 file from raw H.264 IDR keyframe files. Each frame file contains one access unit with multiple NAL units (SPS, PPS, IDR slice) in Annex-B format using 0x00000001 start codes.

func NewH264GoMerger added in v0.7.0

func NewH264GoMerger() *H264GoMerger

NewH264GoMerger creates a new H264GoMerger.

func (*H264GoMerger) CanMerge added in v0.7.0

func (m *H264GoMerger) CanMerge() bool

CanMerge always returns true since this is a pure Go implementation.

func (*H264GoMerger) Merge added in v0.7.0

func (m *H264GoMerger) Merge(ctx context.Context, framesDir, outputPath string, fps int) (*MergeResult, error)

Merge reads H.264 keyframe files from framesDir, builds an MP4 file at outputPath with the given fps, and returns a MergeResult.

func (*H264GoMerger) Tier added in v0.7.0

func (m *H264GoMerger) Tier() MergeTier

Tier returns the merge tier identifier.

type H265GoMerger added in v0.7.0

type H265GoMerger struct{}

H265GoMerger implements TimelapseMerger using pure Go to create an MP4 file from raw H.265 IDR keyframe files. Each frame file contains one access unit with multiple NAL units (VPS, SPS, PPS, IDR slice) in Annex-B format using 0x00000001 start codes.

func NewH265GoMerger added in v0.7.0

func NewH265GoMerger() *H265GoMerger

NewH265GoMerger creates a new H265GoMerger.

func (*H265GoMerger) CanMerge added in v0.7.0

func (m *H265GoMerger) CanMerge() bool

CanMerge always returns true since this is a pure Go implementation.

func (*H265GoMerger) Merge added in v0.7.0

func (m *H265GoMerger) Merge(ctx context.Context, framesDir, outputPath string, fps int) (*MergeResult, error)

Merge reads H.265 keyframe files from framesDir, builds an MP4 file at outputPath with the given fps, and returns a MergeResult.

func (*H265GoMerger) Tier added in v0.7.0

func (m *H265GoMerger) Tier() MergeTier

Tier returns the merge tier identifier.

type IntermediateMP4Pruner added in v0.10.0

type IntermediateMP4Pruner interface {
	ClearMergePathBatch(ctx context.Context, ids []string) error
}

IntermediateMP4Pruner is implemented by storage.DB to let the periodic merger clear the recordings.merge_path pointer for source segments whose intermediate .mp4 output has been pruned. Kept as a separate interface so the shared MergeStatusUpdater (used by the rolling merger too) does not need to grow a periodic-merge-specific method.

type KeyframeExtractor added in v0.7.0

type KeyframeExtractor struct {
	// contains filtered or unexported fields
}

KeyframeExtractor subscribes to a recorder's StreamHub and captures keyframes at configurable intervals, storing them as raw frame files in timelapse segment directories.

It is NOT a recorder — it is a frame consumer that works alongside a regular (non-timelapse) recorder for the same camera. It reuses the recorder's StreamHub for frame delivery and does NOT create its own RTSP connection.

The extractor filters for IDR frames (NAL type 5 for H.264, NAL type 19/20 for H.265) and falls back to P-frames if no IDR arrives within the capture interval.

func NewKeyframeExtractor added in v0.7.0

func NewKeyframeExtractor(cfg KeyframeExtractorConfig) *KeyframeExtractor

NewKeyframeExtractor creates a new KeyframeExtractor with the given config.

func (*KeyframeExtractor) IsRunning added in v0.7.0

func (k *KeyframeExtractor) IsRunning() bool

IsRunning returns whether the extractor is currently active.

func (*KeyframeExtractor) Start added in v0.7.0

func (k *KeyframeExtractor) Start(ctx context.Context, hub *model.StreamHub) error

Start subscribes to the given StreamHub and begins the capture loop. The hub must belong to an active recorder for the same camera. Returns an error if the extractor is already running or if subscription fails.

func (*KeyframeExtractor) Stop added in v0.7.0

func (k *KeyframeExtractor) Stop() error

Stop unsubscribes from the StreamHub and stops the capture loop. Closes the current segment if one is active.

type KeyframeExtractorConfig added in v0.7.0

type KeyframeExtractorConfig struct {
	CameraID   string
	Interval   time.Duration // how often to capture a frame (default: 5s)
	SegmentDur time.Duration // duration of each segment (default: 10min)
	IsH265     bool          // true if the source stream is H.265

	Store    SegmentStore         // required
	DB       RecordingDB          // optional — enables DB recording entries
	MergeMgr *RollingMergeManager // optional — enables rolling merge on segment close

	// CodecParamsProvider returns the recorder's codec parameter sets (SPS/PPS for
	// H.264, VPS/SPS/PPS for H.265). Used as a FALLBACK when parameter sets are not
	// found inline in the frame AU — this happens when the camera sends them
	// out-of-band (in the RTSP DESCRIBE SDP or MP4 moov box) rather than inline
	// with each IDR. Without this fallback, every captured frame file lacks SPS/PPS,
	// and the H264GoMerger/H265GoMerger permanently fails with "frames missing SPS".
	// Optional — when nil, only inline parameter sets are used.
	CodecParamsProvider func() (sps, pps, vps []byte)

	// RecordEnabled gates whether captured frames are written to disk, mirroring
	// the segment recorder's RecordEnabled (internal/recorder/base.go).
	// nil or true = write timelapse frames (default). false = "preview-only":
	// the capture loop keeps ticking (so it self-heals if the flag flips back),
	// but performs no segment/frame I/O — useful when a camera is set to
	// recording_enabled=false and the user expects zero disk writes.
	RecordEnabled *bool
}

KeyframeExtractorConfig holds configuration for the KeyframeExtractor.

type MergeConfig

type MergeConfig struct {
	// Enabled controls whether merging is active for this camera or globally.
	Enabled bool `json:"enabled" yaml:"enabled"`
	// Mode selects the merge output mode (auto, mp4, jpeg).
	Mode MergeMode `json:"mode" yaml:"mode"`
	// OutputFPS controls the output frame rate for merged video.
	// Repurposed from the deprecated TimelapseRecorderConfig.OutputFPS.
	OutputFPS int `json:"output_fps" yaml:"output_fps"`
	// DeleteOriginal removes the source frame directories after a successful merge.
	DeleteOriginal bool `json:"delete_original" yaml:"delete_original"`
	// DailyMerge groups frames by day for daily merged output files.
	DailyMerge bool `json:"daily_merge" yaml:"daily_merge"`
	// CRF controls the Constant Rate Factor for x264/x265 encoding (0-51, default 23).
	// Only applies when using software libx264/libx265 encoder.
	CRF int `json:"crf" yaml:"crf"`
	// Bitrate sets a target bitrate for encoding (e.g. "2M", "500k").
	// Overrides CRF when set; only applies to software libx264/libx265 encoder.
	Bitrate string `json:"bitrate" yaml:"bitrate"`
}

MergeConfig holds merge configuration for timelapse recordings.

type MergeMode

type MergeMode string

MergeMode represents the merge output mode for timelapse recordings.

const (
	// MergeModeAuto auto-detects the best merge mode based on input format.
	MergeModeAuto MergeMode = "auto"
	// MergeModeMP4 merges frames into an MP4 video file.
	MergeModeMP4 MergeMode = "mp4"
	// MergeModeJPEG merges frames into a single JPEG file (e.g., montage).
	MergeModeJPEG MergeMode = "jpeg"
)

func (MergeMode) String

func (m MergeMode) String() string

String returns the string representation of the MergeMode.

type MergeProgressInfo added in v0.7.0

type MergeProgressInfo struct {
	CameraID     string  `json:"camera_id"`
	Progress     int     `json:"progress"`
	Status       string  `json:"status"`
	OutputPath   string  `json:"output_path,omitempty"`
	FramesMerged int     `json:"frames_merged,omitempty"`
	Duration     float64 `json:"duration,omitempty"`
	Tier         string  `json:"tier,omitempty"`
	Error        string  `json:"error,omitempty"`
}

MergeProgressInfo represents the current progress of a merge operation.

type MergeResult

type MergeResult struct {
	// Tier identifies which merge implementation produced this result.
	Tier MergeTier `json:"tier"`
	// OutputPath is the path to the merged output file.
	OutputPath string `json:"output_path"`
	// Error contains the error message if the merge failed.
	Error string `json:"error,omitempty"`
	// FramesMerged is the number of frames successfully merged.
	FramesMerged int `json:"frames_merged"`
	// Duration is the total duration of the merged output in seconds.
	Duration float64 `json:"duration"`
	// Codec is the detected output codec (e.g. "h264", "hevc") from ffprobe.
	Codec string `json:"codec,omitempty"`
}

MergeResult holds the outcome of a merge operation.

type MergeRunFunc added in v0.7.0

type MergeRunFunc func(ctx context.Context, cameraID string, refTime time.Time) error

MergeRunFunc is called by MergeScheduler when a merge is due for a camera. The refTime is the UTC boundary time that triggered the merge.

type MergeScheduler added in v0.7.0

type MergeScheduler struct {
	// contains filtered or unexported fields
}

MergeScheduler schedules periodic merge operations for timelapse cameras. It maintains per-camera entries with configurable intervals and computes next run times based on aligned UTC boundaries. A single goroutine drives the loop, waking up at the earliest next run time across all cameras.

Thread-safe for AddOrUpdate/Remove from any goroutine.

func NewMergeScheduler added in v0.7.0

func NewMergeScheduler(loc *time.Location) *MergeScheduler

NewMergeScheduler creates a new MergeScheduler with no entries. If loc is nil, UTC is used for window alignment. Call SetRunFunc before Start to set the merge callback.

func (*MergeScheduler) AddOrUpdate added in v0.7.0

func (s *MergeScheduler) AddOrUpdate(cameraID string, duration time.Duration)

AddOrUpdate adds or updates a camera's merge schedule. The next run time is computed as the next aligned boundary in the configured timezone.

func (*MergeScheduler) GetDuration added in v0.7.0

func (s *MergeScheduler) GetDuration(cameraID string) (time.Duration, bool)

GetDuration returns the configured merge duration for a camera. Returns false if the camera is not found.

func (*MergeScheduler) Remove added in v0.7.0

func (s *MergeScheduler) Remove(cameraID string)

Remove removes a camera from the merge schedule.

func (*MergeScheduler) SetRunFunc added in v0.7.0

func (s *MergeScheduler) SetRunFunc(fn MergeRunFunc)

SetRunFunc sets the function to call when a merge is due. Must be called before Start.

func (*MergeScheduler) Start added in v0.7.0

func (s *MergeScheduler) Start(ctx context.Context)

Start begins the scheduler loop in a background goroutine. Call Stop to terminate the loop.

func (*MergeScheduler) Stop added in v0.7.0

func (s *MergeScheduler) Stop()

Stop terminates the scheduler loop and waits for it to finish.

func (*MergeScheduler) TriggerDue added in v0.7.0

func (s *MergeScheduler) TriggerDue(ctx context.Context) int

TriggerDue immediately runs merge for all cameras whose nextRun has passed. Returns the number of cameras triggered. Used for testing. Does NOT block on merge completion — merges run in background goroutines.

type MergeStatus

type MergeStatus string

MergeStatus represents the merge process status for a timelapse recording.

const (
	// MergeStatusNone indicates no merge has been attempted.
	MergeStatusNone MergeStatus = "none"
	// MergeStatusMerging indicates a merge is in progress.
	MergeStatusMerging MergeStatus = "merging"
	// MergeStatusMerged indicates the merge completed successfully.
	MergeStatusMerged MergeStatus = "merged"
	// MergeStatusFailed indicates the merge failed.
	MergeStatusFailed MergeStatus = "failed"
)

func (MergeStatus) String

func (s MergeStatus) String() string

String returns the string representation of the MergeStatus.

type MergeStatusUpdater

type MergeStatusUpdater interface {
	SetMergeStatus(ctx context.Context, ids []string, status string) error
	SetMergeResult(ctx context.Context, id string, mergePath, mergeTier string) error
	SetMergeError(ctx context.Context, ids []string, mergeError string) error
	UpdateMergeProgress(ctx context.Context, id string, progress int) error
	UpdateMergeProgressBatch(ctx context.Context, ids []string, progress int) error
}

type MergeTier

type MergeTier string

MergeTier represents the available merge implementation tier.

const (
	// TierFFmpeg uses FFmpeg for merging (requires external binary).
	TierFFmpeg MergeTier = "ffmpeg"
	// TierGo uses native Go implementation for merging.
	TierGo MergeTier = "go"
	// TierJPEG uses native Go JPEG processing for merging.
	TierJPEG MergeTier = "jpeg"
)

func AvailableMergeTier

func AvailableMergeTier() MergeTier

AvailableMergeTier returns the cached merge tier. Returns the zero value ("") if DetectMergeTier has not been called yet.

func DetectMergeTier

func DetectMergeTier(ffmpegPath string, preferFFmpeg ...bool) MergeTier

DetectMergeTier probes the system and selects the best merge implementation tier. Results are cached — only one probe per app lifecycle unless ResetDetectTier is called. Pass ffmpegPath to proactively check a specific FFmpeg binary path, or "" to let the probe search the system PATH. preferFFmpeg=true opts into the FFmpeg tier when available (default false → pure Go).

func (MergeTier) String

func (t MergeTier) String() string

String returns the string representation of the MergeTier.

type Option added in v0.10.0

type Option func(*PeriodicMergeManager)

Option configures PeriodicMergeManager behavior.

func WithDurationLabel added in v0.10.0

func WithDurationLabel(label string) Option

WithDurationLabel records the original config string (e.g. "natural-day", "8h", "7d") so DB rows reflect what the user configured rather than the parsed Go duration. Optional — when unset, Run falls back to duration.String().

func WithIntermediateMP4Pruner added in v0.10.0

func WithIntermediateMP4Pruner(p IntermediateMP4Pruner) Option

WithIntermediateMP4Pruner wires the storage layer used to clear recordings.merge_path after intermediate .mp4 files are pruned. Optional — when nil, finalizeMerge prunes the files but cannot update the DB pointer (the row would still point at a now-deleted path). Production wiring passes the *storage.DB here.

func WithMergeStore added in v0.10.0

func WithMergeStore(s TimelapseMergeStore) Option

WithMergeStore enables persistence of periodic-merge outputs to the timelapse_merges table. When set, Run inserts a 'merging' row at start, completes it on success (with output path, file size, frame count, codec, source segment ids), or marks it failed on error. nil opts out (legacy behavior: file on disk, no DB record).

func WithRecordingEnabledProvider added in v0.10.0

func WithRecordingEnabledProvider(p func(cameraID string) bool) Option

WithRecordingEnabledProvider sets a function that reports if a camera has recording_enabled=true. When true, Run will extract frames from video recordings in the merge window and include them in the timelapse output. The provider is called once per Run invocation with the camera ID. Use functional options pattern so existing call sites need no changes.

func WithRetainIntermediateMP4 added in v0.10.0

func WithRetainIntermediateMP4(retain bool) Option

WithRetainIntermediateMP4 controls whether per-segment rolling-merge .mp4 outputs are kept after a periodic merge folds them into a long-window output. Pass true to retain (debugging / re-merge safety); pass false (the default) to clean them up and reclaim disk. The raw frame directories are always preserved regardless.

type PeriodicMergeManager added in v0.7.0

type PeriodicMergeManager struct {
	// contains filtered or unexported fields
}

PeriodicMergeManager handles merge operations for timelapse recordings with configurable merge intervals (8h, 12h, 24h, 7d, 30d).

func NewPeriodicMergeManager added in v0.7.0

func NewPeriodicMergeManager(store RecordingLister, updater MergeStatusUpdater, merger TimelapseMerger, fps int, dataDir string, duration time.Duration, loc *time.Location, opts ...Option) *PeriodicMergeManager

NewPeriodicMergeManager creates a new PeriodicMergeManager with the given merge duration. If loc is nil, UTC is used for window alignment. Variadic opts enable optional behavior without breaking existing call sites.

func (*PeriodicMergeManager) Duration added in v0.7.0

func (m *PeriodicMergeManager) Duration() time.Duration

Duration returns the configured merge duration.

func (*PeriodicMergeManager) Run added in v0.7.0

func (m *PeriodicMergeManager) Run(ctx context.Context, cameraID string, t time.Time) error

Run executes the merge pipeline for the given camera for the merge window containing the reference time t.

When recording is enabled (recordingEnabledProvider returns true), the method also queries video-format recordings (H264, H265, AVI, MJPEG) in the same window, extracts frames via RecordingFrameExtractor, and merges them alongside existing timelapse recordings. Extracted frames are organized into per-codec temporary directories and cleaned up after merge completion.

type RecordingDB added in v0.7.0

type RecordingDB interface {
	InsertRecording(ctx context.Context, r *model.Recording) error
	InsertRecordingWithRetry(ctx context.Context, r *model.Recording, maxRetries int, backoff time.Duration) error
}

RecordingDB defines the database operations needed for recording metadata.

type RecordingFrameExtractor added in v0.10.0

type RecordingFrameExtractor struct{}

RecordingFrameExtractor extracts frames from recording files at regular intervals. It supports AVI (MJPEG), H264 MP4, and H265 MP4 formats. Output frames are written as frame_000001.ext files in the output directory.

The extractor never loads the full recording file into memory — it uses ReadAt to seek directly to sample offsets, and for AVI it streams chunks sequentially through a ReadSeeker.

func NewRecordingFrameExtractor added in v0.10.0

func NewRecordingFrameExtractor() *RecordingFrameExtractor

NewRecordingFrameExtractor creates a new RecordingFrameExtractor.

func (*RecordingFrameExtractor) ExtractFrames added in v0.10.0

func (e *RecordingFrameExtractor) ExtractFrames(
	filePath string,
	format model.Format,
	interval time.Duration,
	outputDir string,
) (int, error)

ExtractFrames extracts frames from the given recording file at the specified interval. The interval must be positive. For AVI files, frames are JPEG images; for H264/H265 MP4 files, frames are Annex-B NAL streams.

Supported formats: model.FormatAVI, model.FormatH264, model.FormatH265. Returns the number of extracted frames and any error.

type RecordingLister

type RecordingLister interface {
	ListRecordings(ctx context.Context, filter model.RecordingFilter) ([]model.Recording, error)
}

RecordingLister is the interface for listing recordings from the database.

type RollingMergeManager

type RollingMergeManager struct {
	// contains filtered or unexported fields
}

func NewRollingMergeManager

func NewRollingMergeManager(merger TimelapseMerger, db MergeStatusUpdater, fps int, deleteOriginal bool) *RollingMergeManager

func (*RollingMergeManager) ActiveCount

func (r *RollingMergeManager) ActiveCount() int

ActiveCount returns the number of currently active merge goroutines.

func (*RollingMergeManager) GetProgress added in v0.7.0

func (r *RollingMergeManager) GetProgress(cameraID string) (MergeProgressInfo, bool)

GetProgress returns the current progress for a camera merge. Returns the progress info and true if a merge is or was tracked for this camera.

func (*RollingMergeManager) IsActive

func (r *RollingMergeManager) IsActive(cameraID string) bool

IsActive returns true if there is an active merge for the given camera.

func (*RollingMergeManager) StartSegmentMerge

func (r *RollingMergeManager) StartSegmentMerge(ctx context.Context, cameraID, segmentDir, outputPath, recordingID string)

StartSegmentMerge launches an async goroutine that waits for the segment to complete (via ctx cancellation or a done signal), then calls Merge(). The caller should cancel ctx when the segment is closed.

func (*RollingMergeManager) StopAll

func (r *RollingMergeManager) StopAll()

StopAll cancels all active merge goroutines and waits for them to fully exit.

Waiting is required to honor the App.Service contract ("Stop must release all goroutines"): in-flight runMerge goroutines touch r.merger and r.db, which the caller may begin tearing down once StopAll returns (#163). Without the wait, those goroutines could briefly outlive the resources they reference.

The cancel+clear happens under r.mu, but the Wait is OUTSIDE the lock — runMerge's cleanup defer also acquires r.mu, so waiting under the lock would deadlock. A concurrent StartSegmentMerge may add a fresh entry after the clear; that goroutine is tracked by wg too, so Wait still returns promptly (mergers honor ctx cancel) and the new entry is left in r.active for the next lifecycle — matching the existing "final state may have active entries" semantics documented in TestRollingMergeManager_ConcurrentStopAllAndStart.

r.stopMu serializes overlapping StopAll calls so that a second StopAll cannot call r.wg.Wait() while the wg counter is between zero (drained by a first Wait) and a fresh Add(1) from a racing StartSegmentMerge — which would panic with "WaitGroup is reused before previous Wait has returned".

func (*RollingMergeManager) StopSegmentMerge

func (r *RollingMergeManager) StopSegmentMerge(cameraID string)

type Scheduler added in v0.7.0

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler evaluates timelapse recording schedules based on current time. Thread-safe when used read-only after config load.

func NewScheduler added in v0.7.0

func NewScheduler(loc *time.Location) *Scheduler

NewScheduler creates a new Scheduler with default time source.

func (*Scheduler) IsRecordingTime added in v0.7.0

func (s *Scheduler) IsRecordingTime(cfg config.CameraTimelapseConfig) bool

IsRecordingTime reports whether timelapse recording should be active based on the current time in the scheduler's timezone and the given schedule configuration.

Rules:

  • If Paused is true, always returns false.
  • If Schedule is nil (no schedule), returns true (24/7 recording).
  • If Schedule exists but DaysOfWeek is empty, all days are allowed.
  • If Schedule exists but TimeRanges is empty, recording is all-day on allowed days.
  • Otherwise, returns true only if the current day matches DaysOfWeek AND current time falls within any configured TimeRange.

func (*Scheduler) IsScheduleActive added in v0.9.0

func (s *Scheduler) IsScheduleActive(schedule *config.ScheduleConfig) bool

IsScheduleActive checks if the current time falls within the given ScheduleConfig. This is the generic version of IsRecordingTime that works with any ScheduleConfig (not just CameraTimelapseConfig), enabling reuse for recording schedules.

func (*Scheduler) NextTransition added in v0.7.0

func (s *Scheduler) NextTransition(cfg config.CameraTimelapseConfig) time.Duration

NextTransition returns the duration until the next schedule state change (recording ↔ not recording). Returns 0 when:

  • The schedule is nil (always recording — no transitions).
  • The schedule is paused (always not recording — no transitions).
  • No future transition is found within 7 days.

The duration is computed at minute granularity matching the schedule definition (HH:MM).

func (*Scheduler) SetClockForTesting added in v0.10.0

func (s *Scheduler) SetClockForTesting(now func() time.Time)

SetClockForTesting injects a fixed time source for deterministic tests. Pass nil to restore the default (time.Now().In(s.loc)). Test-only helper — do not call in production code. Solves wall-clock flakiness where a schedule window (e.g. 00:00-00:01) deterministically matches when CI runs at midnight UTC (issue #151).

type SegmentStore added in v0.7.0

type SegmentStore interface {
	CreateSegment(cameraID string, format string) (tempPath string, finalPath string, err error)
	CloseSegment(tempPath, finalPath string) error
}

SegmentStore defines the segment lifecycle interface needed by KeyframeExtractor. This is a subset of the recorder.SegmentStore interface — defined locally to avoid circular imports (recorder imports timelapse).

type SnapshotCapturer added in v0.7.0

type SnapshotCapturer struct {
	// contains filtered or unexported fields
}

SnapshotCapturer captures JPEG snapshots from an HTTP URL at a configurable interval and writes them as frame sequences in segment directories. Implements model.Recorder.

func NewSnapshotCapturer added in v0.7.0

func NewSnapshotCapturer(cfg SnapshotCapturerConfig, store SegmentStore, opts ...*metrics.Metrics) *SnapshotCapturer

NewSnapshotCapturer creates a new SnapshotCapturer.

func (*SnapshotCapturer) Start added in v0.7.0

func (r *SnapshotCapturer) Start(ctx context.Context) error

Start begins the snapshot capture loop.

func (*SnapshotCapturer) Status added in v0.7.0

Status returns the current recorder status.

func (*SnapshotCapturer) Stop added in v0.7.0

func (r *SnapshotCapturer) Stop() error

Stop stops the snapshot capture loop and closes the current segment.

type SnapshotCapturerConfig added in v0.7.0

type SnapshotCapturerConfig struct {
	CameraID    string
	SnapshotURL string // HTTP URL for snapshot; if empty, DeriveSnapshotURL is used
	Interval    time.Duration
	SegmentDur  time.Duration
	Username    string // optional basic auth username
	Password    string // optional basic auth password
	DB          RecordingDB
	Store       SegmentStore
	Metrics     *metrics.Metrics
	MergeMgr    *RollingMergeManager // optional rolling merge manager
	Protocol    string               // camera protocol for DeriveSnapshotURL fallback
	StreamURL   string               // camera stream URL for DeriveSnapshotURL fallback

	// FrameProvider, when set, supplies JPEG frames from an in-memory cache
	// (e.g. HTTPJPEGRecorder.LatestFrame) instead of an HTTP GET. This is used
	// for dual-mode MJPEG/JPEG timelapse: it reuses the already-running
	// recorder's latest frame without opening a second network connection
	// (critical for ESP32 cameras with very limited concurrent HTTP capacity).
	// When set, SnapshotURL is not required and the HTTP client is unused.
	FrameProvider func() []byte

	// RecordEnabled gates whether captured frames are written to disk, mirroring
	// the segment recorder's RecordEnabled (internal/recorder/base.go).
	// nil or true = write timelapse frames (default). false = "preview-only":
	// the capture loop keeps ticking (so it self-heals if the flag flips back),
	// but performs no segment/frame I/O — useful when a camera is set to
	// recording_enabled=false and the user expects zero disk writes.
	RecordEnabled *bool
}

SnapshotCapturerConfig holds configuration for the HTTP snapshot capturer.

type TimelapseMergeStore added in v0.10.0

type TimelapseMergeStore interface {
	InsertTimelapseMerge(ctx context.Context, m *model.TimelapseMerge) (int64, error)
	UpdateTimelapseMergeStatus(ctx context.Context, id int64, status, errMsg string) error
	CompleteTimelapseMerge(ctx context.Context, id int64, outputPath string, fileSize int64, frameCount int, codec, sourceSegmentIDs string) error
	FindTimelapseMergeByWindow(ctx context.Context, cameraID string, windowStart time.Time, durationLabel string) (*model.TimelapseMerge, error)
}

TimelapseMergeStore is the interface for persisting periodic-merge output metadata to the timelapse_merges table. Implementations must be safe for concurrent use (one Run per camera, but API-triggered merges may race with scheduled ones).

type TimelapseMerger

type TimelapseMerger interface {
	// CanMerge reports whether this merge tier is available (e.g., binary present, codec supported).
	CanMerge() bool
	// Merge performs the merge of frame files from framesDir into outputPath at the given fps.
	Merge(ctx context.Context, framesDir, outputPath string, fps int) (*MergeResult, error)
	// Tier returns the merge tier identifier.
	Tier() MergeTier
}

TimelapseMerger is the interface for merging timelapse frame sequences into a single output file.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL