transcoding

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: 27 Imported by: 0

Documentation

Overview

Package transcoding provides hardware-aware video transcoding for stored recordings.

It supports converting between H.264, H.265, and MJPEG formats using FFmpeg as the backend transcoder. The package performs automatic hardware capability detection on startup, checking for available encoders (V4L2M2M, VAAPI, NVENC) and falling back to software encoding (libx264/libx265) when appropriate.

Key features:

  • Hardware capability self-check (detects available encoders, cores, memory)
  • Async task queue with progress tracking
  • Multi-format support (H.264 ↔ H.265, MJPEG → H.264)
  • Per-camera transcoding configuration
  • FFmpeg download management (static binary for ARM)

Architecture overview:

Manager (singleton) → Hardware probe → Task queue → Worker pool → FFmpeg exec
         ↕                           ↕
  REST API handler              DB (SQLite)

The Manager coordinates probe results, queues tasks, and manages worker goroutines. Each worker invokes FFmpeg as a subprocess with appropriate encoder flags based on the hardware capabilities detected at startup.

Module path: github.com/Mi-Bee-Studio/MiBeeNvr

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildFFmpegCommand

func BuildFFmpegCommand(opts TranscodeOptions, caps HardwareCapabilities) ([]string, error)

BuildFFmpegCommand constructs the FFmpeg argument array for a transcode job. Returns the complete argument list ready for exec.Command.

func CheckSufficient

func CheckSufficient(caps *HardwareCapabilities, requiredCodec string) (bool, string)

CheckSufficient checks whether the hardware can handle transcoding for the given codec. Returns (true, "") if sufficient, or (false, reason) if not.

func CleanOrphanedTranscodes

func CleanOrphanedTranscodes(ctx context.Context, dataDir string, db DBTaskLister) error

CleanOrphanedTranscodes walks dataDir for files matching *.transcoded.mp4 and deletes any that do not have a corresponding task in the database. This handles crash recovery: orphaned output files left from tasks that were never recorded in DB (e.g., process died mid-enqueue) or whose tasks were cleaned up by DeleteCompletedTasks.

func GetDisabledReason

func GetDisabledReason() string

GetDisabledReason returns the stored disabled reason. Returns empty string if transcoding was never disabled or reason was cleared.

func ResetProbe

func ResetProbe()

ResetProbe clears the cached probe result (for testing). Exported because cross-package tests (e.g. internal/timelapse) need to reset the cache between subtests.

func SetDisabledReason

func SetDisabledReason(reason string)

SetDisabledReason stores the reason why transcoding was disabled. Thread-safe. Called from main.go when NewTranscodeManager fails.

Types

type CompletionFunc added in v0.6.0

type CompletionFunc func(task *storage.TranscodeTask, success bool)

CompletionFunc is called after a transcode task finishes (success or failure). Implementations handle post-processing like DB registration or file cleanup.

type DBTaskLister

type DBTaskLister interface {
	ListTranscodeTasks(ctx context.Context, f storage.TranscodeTaskFilter) ([]storage.TranscodeTask, int, error)
}

DBTaskLister abstracts the database operations needed for orphan cleanup.

type DownloadState

type DownloadState struct {
	Status          string  `json:"status"`
	Progress        float64 `json:"progress"`
	Version         string  `json:"version"`
	Error           string  `json:"error"`
	DownloadURL     string  `json:"download_url,omitempty"`
	LastUpdated     string  `json:"last_updated"`
	TotalBytes      int64   `json:"total_bytes,omitempty"`
	DownloadedBytes int64   `json:"downloaded_bytes,omitempty"`
}

DownloadState persists download progress across restarts.

type DownloadStatus

type DownloadStatus struct {
	Status          string  `json:"status"`   // "not_installed", "downloading", "available", "failed"
	Progress        float64 `json:"progress"` // 0.0-1.0
	Version         string  `json:"version"`
	Error           string  `json:"error"`
	TotalBytes      int64   `json:"total_bytes"`      // total size of download in bytes
	DownloadedBytes int64   `json:"downloaded_bytes"` // bytes downloaded so far
}

DownloadStatus represents the FFmpeg download state for the frontend.

type Downloader

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

Downloader manages FFmpeg static binary downloads. It is safe for concurrent use — a mutex prevents duplicate downloads.

func NewDownloader

func NewDownloader(dataDir string, onProgress func(downloaded, total int64)) *Downloader

NewDownloader creates a new FFmpeg downloader. dataDir is the storage root (e.g. StorageConfig.RootDir). onProgress is an optional callback invoked with bytes downloaded and total.

func (*Downloader) Cancel

func (d *Downloader) Cancel()

Cancel cancels an active download. Safe to call when nothing is downloading.

func (*Downloader) DownloadFFmpeg

func (d *Downloader) DownloadFFmpeg(ctx context.Context) error

DownloadFFmpeg downloads FFmpeg static binary. Idempotent: if FFmpeg exists and is valid, returns nil immediately. Concurrent-safe: a second call while downloading returns an error.

func (*Downloader) FFmpegPath

func (d *Downloader) FFmpegPath() string

FFmpegPath returns the expected FFmpeg binary path.

func (*Downloader) FFprobePath

func (d *Downloader) FFprobePath() string

FFprobePath returns the expected ffprobe binary path.

func (*Downloader) GetFFmpegStatus

func (d *Downloader) GetFFmpegStatus() DownloadStatus

GetFFmpegStatus checks FFmpeg availability and download status. It checks: 1) system PATH, 2) custom {dataDir}/tools/ffmpeg, 3) download state.

func (*Downloader) StatePath

func (d *Downloader) StatePath() string

StatePath returns the path for download state persistence.

type EncoderType

type EncoderType string

EncoderType represents the encoder backend.

const (
	EncoderSoftware EncoderType = "software"
	EncoderV4L2M2M  EncoderType = "v4l2m2m"
	EncoderVAAPI    EncoderType = "vaapi"
	EncoderNVENC    EncoderType = "nvenc"
)

type HardwareCapabilities

type HardwareCapabilities struct {
	Arch                 string      `json:"arch"`
	TotalCores           int         `json:"total_cores"`
	TotalMemoryMB        uint64      `json:"total_memory_mb"`
	H264Encoder          string      `json:"h264_encoder"`           // encoder name (e.g. "libx264", "h264_v4l2m2m")
	H265Encoder          string      `json:"h265_encoder"`           // encoder name (e.g. "libx265", "h265_v4l2m2m")
	H264EncoderType      EncoderType `json:"h264_encoder_type"`      // backend type
	H265EncoderType      EncoderType `json:"h265_encoder_type"`      // backend type
	H264Decoder          string      `json:"h264_decoder"`           // decoder name (e.g. "h264_v4l2m2m", "" means software-only)
	H265Decoder          string      `json:"h265_decoder"`           // decoder name (e.g. "hevc_v4l2m2m", "" means software-only)
	H264DecoderType      EncoderType `json:"h264_decoder_type"`      // reuse EncoderType enum for decoders
	H265DecoderType      EncoderType `json:"h265_decoder_type"`      // reuse EncoderType enum for decoders
	MaxEncodeWidth       int         `json:"max_encode_width"`       // max output width supported by encoder (0 = unlimited)
	MaxEncodeHeight      int         `json:"max_encode_height"`      // max output height supported by encoder (0 = unlimited)
	Devices              []string    `json:"devices"`                // /dev/video* paths
	MaxConcurrentStreams int         `json:"max_concurrent_streams"` // estimated safe concurrency
	EstimatedFPS         float64     `json:"estimated_fps"`          // estimated encoding FPS
	FFmpegAvailable      bool        `json:"ffmpeg_available"`       // whether FFmpeg is installed
	FFmpegPath           string      `json:"ffmpeg_path"`            // path to FFmpeg binary
}

HardwareCapabilities holds the results of a hardware probe.

func ProbeHardwareCapabilities

func ProbeHardwareCapabilities(ffmpegPath string) *HardwareCapabilities

ProbeHardwareCapabilities probes the system for transcoding capabilities. Uses sync.Once for idempotent caching — zero overhead after first call.

func ProbeHardwareCapabilitiesExplicit added in v0.7.0

func ProbeHardwareCapabilitiesExplicit(ffmpegPath string) *HardwareCapabilities

ProbeHardwareCapabilitiesExplicit probes with a specific FFmpeg path without caching. Used when FFmpeg was downloaded after startup and the cached probe result is stale.

type ManagerConfig

type ManagerConfig struct {
	Transcoding     config.TranscodingConfig
	DataDir         string
	FFmpegPath      string
	FFprobePath     string
	MaxWorkers      int
	ReplaceOriginal bool
	EventBus        *event.EventBus
	Config          *config.Config
}

ManagerConfig holds the dependencies for TranscodeManager.

type ManagerStatus

type ManagerStatus struct {
	Enabled        bool                  `json:"enabled"`
	DisabledReason string                `json:"disabled_reason"`
	Hardware       *HardwareCapabilities `json:"hardware"`
	QueueLength    int                   `json:"queue_length"`
	ActiveJobs     int                   `json:"active_jobs"`
	RecentResults  []TranscodeTask       `json:"recent_results"`
}

ManagerStatus is returned by the API status endpoint.

type MediaInfo

type MediaInfo struct {
	CodecName string  `json:"codec_name"`
	Duration  float64 `json:"duration"`
	Width     int     `json:"width"`
	Height    int     `json:"height"`
}

MediaInfo holds the result of an ffprobe invocation.

func GetMediaInfo

func GetMediaInfo(ffprobePath, filePath string) (*MediaInfo, error)

GetMediaInfo extracts codec, duration, and resolution from a media file.

It prefers the pure-Go mediaprobe path (no external process, reads only MP4 box metadata) for MP4 files, and falls back to ffprobe when mediaprobe fails or the input is not MP4. This removes the hard ffprobe dependency for the common case while keeping ffprobe as a fallback for edge cases (non-MP4 containers, corrupted moov, etc.).

When ffprobePath is empty, ffprobe is looked up on PATH; if it is not available the ffprobe fallback is skipped and only the mediaprobe result is returned (which may be an error for non-MP4 inputs).

type QueueAPI

type QueueAPI interface {
	Enqueue(ctx context.Context, task *storage.TranscodeTask) error
	CancelTask(ctx context.Context, id int64) error
}

QueueAPI defines the interface for transcode queue operations. Both TranscodeQueue and test mocks implement this interface.

type QueueConfig

type QueueConfig struct {
	DataDir         string // root data directory for orphan cleanup
	MaxWorkers      int
	FFmpegPath      string
	FFprobePath     string
	ReplaceOriginal bool
	JobTimeout      time.Duration // per-job timeout, 0 means no timeout
}

QueueConfig holds configuration for the transcode queue.

type TranscodeManager

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

TranscodeManager is the top-level manager for the transcoding subsystem. Follows the MergeManager lifecycle pattern: New → Run(ctx) → Stop. It is nil-safe: all methods handle nil receivers gracefully so callers don't need nil checks (e.g. app.transcodeMgr.Stop() is safe when disabled).

func NewTranscodeManager

func NewTranscodeManager(store *storage.DB, cfg ManagerConfig, m *metrics.Metrics) (*TranscodeManager, error)

NewTranscodeManager probes hardware, validates capabilities, and creates the manager. Returns an error if the self-check fails (FFmpeg missing, no encoder, etc.). The caller should check the error and only assign the manager on success.

func (*TranscodeManager) Downloader

func (m *TranscodeManager) Downloader() *Downloader

Downloader returns the FFmpeg downloader for API access. Returns nil if the manager is nil (transcoding disabled).

func (*TranscodeManager) EnqueueRecording

func (m *TranscodeManager) EnqueueRecording(cameraID, recordingID, inputPath, inputFormat string, targetCodec, bitrate string, crf int) error

EnqueueRecording creates a transcode task for a completed recording segment. The call is non-blocking — the task is inserted into the database queue and will be picked up by a worker goroutine. bitrate and crf come from the per-camera transcoding config (0/empty = use defaults).

func (*TranscodeManager) GetStatus

func (m *TranscodeManager) GetStatus() ManagerStatus

GetStatus returns the current manager and queue status.

func (*TranscodeManager) HardwareInfo

func (m *TranscodeManager) HardwareInfo() *HardwareCapabilities

HardwareInfo returns the probed hardware capabilities. Returns nil if the manager is nil (transcoding disabled).

func (*TranscodeManager) Queue

func (m *TranscodeManager) Queue() QueueAPI

Queue returns the transcode queue for API access (cancel, status, etc.). Returns nil if the manager is nil (transcoding disabled).

func (*TranscodeManager) Run

func (m *TranscodeManager) Run(ctx context.Context)

Run starts the transcoding queue workers and the FFmpeg status watcher. Blocks until ctx is cancelled.

func (*TranscodeManager) Stop

func (m *TranscodeManager) Stop()

Stop cancels the manager context and gracefully drains the queue.

type TranscodeOptions

type TranscodeOptions struct {
	InputPath     string `json:"input_path"`
	OutputPath    string `json:"output_path"`
	InputCodec    string `json:"input_codec"`    // "h264", "h265", "mjpeg"
	OutputCodec   string `json:"output_codec"`   // "h264", "h265"
	Width         int    `json:"width"`          // output width
	Height        int    `json:"height"`         // output height
	Bitrate       string `json:"bitrate"`        // e.g. "2M"
	Framerate     int    `json:"framerate"`      // output framerate
	ForceSoftware bool   `json:"force_software"` // bypass hardware encoders
	Preset        string `json:"preset"`         // "ultrafast", "faster", "medium"
	// CRF (Constant Rate Factor) controls quality for libx264/libx265 software encoders.
	// Lower = higher quality + larger file. Range 0-51 (libx264 default 23, libx265 28).
	// 0 means use the encoder's default (no explicit -crf flag override beyond the default).
	CRF int `json:"crf"`
}

TranscodeOptions holds parameters for building an FFmpeg command.

type TranscodeQueue

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

TranscodeQueue manages async transcoding tasks with a bounded worker pool. Tasks are dequeued from the database (FIFO) and dispatched to worker goroutines.

func NewTranscodeQueue

func NewTranscodeQueue(store *storage.DB, caps *HardwareCapabilities, dl *Downloader, cfg QueueConfig, m *metrics.Metrics) *TranscodeQueue

NewTranscodeQueue creates a new TranscodeQueue.

func (*TranscodeQueue) ActiveCount

func (q *TranscodeQueue) ActiveCount() int

ActiveCount returns the number of currently running tasks.

func (*TranscodeQueue) CancelTask

func (q *TranscodeQueue) CancelTask(ctx context.Context, id int64) error

CancelTask cancels a running or pending task. For running tasks, it kills the FFmpeg process via the stored cancel function.

func (*TranscodeQueue) Enqueue

func (q *TranscodeQueue) Enqueue(ctx context.Context, task *storage.TranscodeTask) error

Enqueue inserts a new pending task into the database. Rejects tasks that would require software encoding on ARM architecture. Rejects tasks with input codecs that lack hardware decoders on ARM. Rejects tasks where input resolution exceeds encoder limits.

func (*TranscodeQueue) GetStatus

func (q *TranscodeQueue) GetStatus() ManagerStatus

GetStatus returns the current queue status for the API.

func (*TranscodeQueue) Run

func (q *TranscodeQueue) Run(ctx context.Context) error

Run starts the queue's main polling loop. It blocks until ctx is cancelled. On each tick it checks for pending tasks and dispatches up to MaxWorkers concurrently.

func (*TranscodeQueue) SetCompletionFunc added in v0.6.0

func (q *TranscodeQueue) SetCompletionFunc(fn CompletionFunc)

SetCompletionFunc registers a callback invoked after each task completes. The callback receives the task and whether it succeeded.

func (*TranscodeQueue) Stop

func (q *TranscodeQueue) Stop()

Stop gracefully drains the queue. Active workers are allowed to finish. New tasks will not be dequeued after this call.

type TranscodeStatus

type TranscodeStatus string

TranscodeStatus represents the status of a transcoding task.

const (
	StatusPending   TranscodeStatus = "pending"
	StatusRunning   TranscodeStatus = "running"
	StatusCompleted TranscodeStatus = "completed"
	StatusFailed    TranscodeStatus = "failed"
	StatusCancelled TranscodeStatus = "cancelled"
)

type TranscodeTask

type TranscodeTask struct {
	ID              int64           `json:"id"`
	CameraID        string          `json:"camera_id"`
	RecordingID     string          `json:"recording_id"`
	InputPath       string          `json:"input_path"`
	InputFormat     string          `json:"input_format"`
	OutputPath      string          `json:"output_path"`
	OutputFormat    string          `json:"output_format"`
	Status          TranscodeStatus `json:"status"`
	Progress        float64         `json:"progress"`
	Error           string          `json:"error"`
	CreatedAt       string          `json:"created_at"`
	StartedAt       *string         `json:"started_at"`
	CompletedAt     *string         `json:"completed_at"`
	OriginalDeleted bool            `json:"original_deleted"`
}

TranscodeTask represents a transcoding task stored in the database.

Jump to

Keyboard shortcuts

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