Documentation
¶
Overview ¶
Package transcode probes the host environment for hardware-accelerated video encoding/decoding options and exposes a runtime capability matrix.
Design goal: portability — swap GPUs, drop GPU entirely, install a different driver, and the probe re-evaluates. Never hard-code encoder names in handlers.
Index ¶
- func ParseSegIndex(name string) (int, bool)
- func ResetCachedForTesting()
- func Run(ctx context.Context, in io.Reader, w http.ResponseWriter, opts Options) error
- func SetCachedForTesting(c *Capabilities)
- type Capabilities
- type Decoder
- type Encoder
- type HLSSession
- type HLSSessionManager
- func (m *HLSSessionManager) Close(key string)
- func (m *HLSSessionManager) CloseForHash(hashHex string)
- func (m *HLSSessionManager) EffectiveKey(rawKey string, nativeHLS bool) string
- func (m *HLSSessionManager) GetOrStart(ctx context.Context, opts HLSStartOpts) (*HLSSession, error)
- func (m *HLSSessionManager) Peek(key string) (*HLSSession, error)
- func (m *HLSSessionManager) Sessions() []HLSSessionSnapshot
- func (m *HLSSessionManager) SetGPUTranscodeLimit(limit int)
- func (m *HLSSessionManager) SetVODMode(mode VODMode)
- func (m *HLSSessionManager) Stop()
- type HLSSessionSnapshot
- type HLSStartOpts
- type Options
- type VODMode
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ParseSegIndex ¶
ParseSegIndex is the exported form of parseSegName for handlers that need to map a requested segment filename back to its index.
func ResetCachedForTesting ¶
func ResetCachedForTesting()
ResetCachedForTesting zera o cache de capabilities. Só para testes que precisam exercitar o caminho "caps ainda não probadas".
func Run ¶
Run pipes an input ReadSeeker through ffmpeg with the chosen options and streams to w. On failure, the stderr tail is logged so we can diagnose pipeline issues.
Pre-warm: we read a small chunk (~256 KiB) from the input before invoking ffmpeg, then concatenate that buffer with the rest of the stream. This catches two failure modes early:
- anacrolix Reader returns immediately with an error (torrent dropped, reader closed, piece 0 not available). We return 503 with a clear message instead of letting ffmpeg parse-error on EOF.
- Source bytes arrive too late and ffmpeg parses corrupt input. Pre-warm means by the time ffmpeg sees byte 0, we already have a valid prefix.
256 KiB is enough to cover MKV/MP4 headers + first cluster on most files, while staying small enough that the warm-up doesn't add noticeable latency to the user-visible "Loading..." spinner.
func SetCachedForTesting ¶
func SetCachedForTesting(c *Capabilities)
SetCachedForTesting injeta uma matriz de capabilities fake (ex.: um script stub no lugar do ffmpeg) para testes — inclusive de OUTROS pacotes — que precisam iniciar sessões HLS sem probar o host. Parear com ResetCachedForTesting no cleanup.
Types ¶
type Capabilities ¶
type Capabilities struct {
ProbedAt time.Time `json:"probedAt"`
OS string `json:"os"`
FFmpegPath string `json:"ffmpegPath"`
FFmpegVer string `json:"ffmpegVersion"`
HasNVIDIA bool `json:"hasNvidia"`
HasVAAPI bool `json:"hasVaapi"`
HasQSV bool `json:"hasQsv"`
Encoders []Encoder `json:"encoders"`
Decoders []Decoder `json:"decoders"`
Preferred string `json:"preferred"` // chosen encoder ID for H.264 transcoding
PreferredHE string `json:"preferredHevc"` // chosen for HEVC transcoding (if any)
}
Capabilities is the full probe result.
func Cached ¶
func Cached() *Capabilities
Cached returns the last probe result without re-running it. nil if never probed.
func Probe ¶
func Probe(ctx context.Context, force bool) (*Capabilities, error)
Probe runs the full detection + smoke-test sequence and caches the result. Pass force=true to re-probe (e.g. after a GPU upgrade).
func (*Capabilities) String ¶
func (c *Capabilities) String() string
String returns a one-line summary suitable for logs.
type Decoder ¶
type Decoder struct {
ID string `json:"id"`
Codec string `json:"codec"`
Backend string `json:"backend"`
Available bool `json:"available"`
Functional bool `json:"functional"`
Error string `json:"error,omitempty"`
}
Decoder is the same shape but for decoding.
type Encoder ¶
type Encoder struct {
ID string `json:"id"` // stable identifier (e.g. "h264_nvenc")
Codec string `json:"codec"` // "h264" | "hevc"
Backend string `json:"backend"` // "nvidia" | "amd-vaapi" | "intel-qsv" | "cpu"
Available bool `json:"available"` // listed by ffmpeg as compiled in
Functional bool `json:"functional"` // smoke-test encode succeeded
BenchFPS float64 `json:"benchFps,omitempty"` // frames/sec on 480p test clip
Description string `json:"description"`
Error string `json:"error,omitempty"`
}
Encoder identifies one transcoding backend.
type HLSSession ¶
type HLSSession struct {
Key string
Dir string
Cmd *exec.Cmd
Cancel context.CancelFunc
StartedAt time.Time
LastAccess time.Time
// DurationSec is the total media duration, probed (seekably) once at
// startup. 0 means "unknown" — the source's moov/Cues weren't reachable
// in time, so callers must fall back to the live/EVENT playlist instead
// of generating a finite VOD playlist.
DurationSec float64
// contains filtered or unexported fields
}
VODMode is defined in hls_vod.go. HLSSession is a single ongoing HLS transcode. Same key = same session (deduped across concurrent requests for the same content).
func (*HLSSession) EnsureSegment ¶
func (s *HLSSession) EnsureSegment(idx int)
EnsureSegment makes sure an encoder is (or will soon be) producing segment `idx`. The segment handler calls this when `idx` isn't on disk yet. It only restarts the encoder for a real seek: backward (idx < startSeg — the encoder already passed it and won't return) or a far-forward jump (beyond the read-ahead window). Everything in between is normal buffering — the running sequential encoder will reach it, so we let the caller wait.
func (*HLSSession) IsVOD ¶
func (s *HLSSession) IsVOD() bool
IsVOD reports whether this session serves a finite VOD playlist (full seekbar) vs the incremental EVENT/live playlist. Decided once at start from the VOD policy + known duration; handler and encoder read this single flag.
func (*HLSSession) RestartAt ¶
func (s *HLSSession) RestartAt(seg int) error
RestartAt relaunches ffmpeg to begin producing at segment `seg`. Only meaningful in VOD mode. The decision of WHETHER to restart lives in EnsureSegment; this just performs it, serialised so concurrent segment requests can't spawn duplicate encoders. No-op when already encoding from `seg`. Older segments on disk are kept so backward seeks reuse them.
func (*HLSSession) WaitForMaster ¶
func (s *HLSSession) WaitForMaster(timeout time.Duration) error
WaitForMaster blocks up to `timeout` waiting for the master `index.m3u8` to appear. ffmpeg only writes the playlist after the first segment is completely encoded, so the wait is bounded by `-hls_time 4` plus encoder startup. We bail if the session ends without writing one.
func (*HLSSession) WaitForSegment ¶
WaitForSegment blocks for the named segment file (basename only) to exist and be fully written. ffmpeg's `temp_file` flag means segments appear atomically — once `seg_NNN.ts` is visible, it's complete.
type HLSSessionManager ¶
type HLSSessionManager struct {
// contains filtered or unexported fields
}
HLSSessionManager owns the lifecycle of ffmpeg-driven HLS transcoding sessions. One session per (info_hash, file_index, encoder options) tuple — concurrent viewers of the same content share a single ffmpeg + segments dir.
Why HLS specifically: Safari (macOS + iOS) refuses progressive fragmented MP4 via <video src> with chunked transfer encoding. Empirically every combination of -movflags, profile, level, GOP, B-frame count we tried produced bytes Safari rejects with MediaError.SRC_NOT_SUPPORTED before any frames decode. Apple's documented streaming path is HLS (.m3u8 + .ts segments) — `<video src="...m3u8">` is the only thing Safari treats as a first-class video source. Jellyfin / Plex / Emby all do this for browser clients. Stop trial-and-error, follow Apple's contract.
Trade-off vs progressive MP4: needs disk space for ~2-segment-buffer per active session (~20-40 MB at 720p, ~80 MB at 1080p) and a small directory per session. ffmpeg writes segments to disk as it encodes; the handler serves them on request. Cleanup on idle keeps the footprint bounded.
func NewHLSManager ¶
func NewHLSManager(baseDir string) (*HLSSessionManager, error)
NewHLSManager constructs a manager rooted at baseDir/hls/. The directory is created on demand; existing contents (from previous server runs) are purged to avoid serving stale segments tied to old encoder options.
func (*HLSSessionManager) Close ¶
func (m *HLSSessionManager) Close(key string)
Close terminates the session immediately. Called by handlers when the underlying torrent is dropped or the user explicitly cancels.
func (*HLSSessionManager) CloseForHash ¶
func (m *HLSSessionManager) CloseForHash(hashHex string)
CloseForHash para TODAS as sessões HLS de um torrent (keys "<hash>-<fileIdx>"). Chamado quando o player fecha (Drop) pra não deixar o ffmpeg do transcode órfão consumindo CPU até o idle-reaper (5min). Idempotente; no-op se não houver.
func (*HLSSessionManager) EffectiveKey ¶
func (m *HLSSessionManager) EffectiveKey(rawKey string, nativeHLS bool) string
EffectiveKey maps a raw content key to the session key actually used. When VOD is off the key is unchanged (one shared EVENT session per content, zero behaviour change). When VOD is on, VOD-eligible and non-eligible clients are split into distinct sessions (-vod/-evt) so a VOD session created by one client never serves a VOD playlist to a client that must stay on EVENT (the Safari #61 safeguard). Master and segment handlers must agree on this.
func (*HLSSessionManager) GetOrStart ¶
func (m *HLSSessionManager) GetOrStart(ctx context.Context, opts HLSStartOpts) (*HLSSession, error)
Start/build de sessão HLS (GetOrStart, buildSession, reserva de GPU) — extraído de hls.go. GetOrStart returns an existing session keyed by opts.Key or starts a new one. On new-session, ffmpeg begins encoding immediately; the caller should poll for index.m3u8 to appear via WaitForMaster.
Ownership: GetOrStart takes opts.Source. A new session keeps it (and closes it on stop); when an existing session is returned or creation fails, the source is closed here (io.Closer sources only). Callers must NOT close it.
func (*HLSSessionManager) Peek ¶
func (m *HLSSessionManager) Peek(key string) (*HLSSession, error)
Peek returns an existing session without starting one. Used by the segment handler which must NOT race the playlist handler into creating a duplicate ffmpeg. Returns an error when the session isn't tracked.
func (*HLSSessionManager) Sessions ¶
func (m *HLSSessionManager) Sessions() []HLSSessionSnapshot
Sessions returns all currently active transcode sessions in the manager.
func (*HLSSessionManager) SetGPUTranscodeLimit ¶
func (m *HLSSessionManager) SetGPUTranscodeLimit(limit int)
SetGPUTranscodeLimit overrides the concurrent HW-decode cap (tests; or a future live-config path). limit <= 0 means unlimited.
func (*HLSSessionManager) SetVODMode ¶
func (m *HLSSessionManager) SetVODMode(mode VODMode)
SetVODMode sets the VOD policy (called once at wiring time from config).
func (*HLSSessionManager) Stop ¶
func (m *HLSSessionManager) Stop()
Stop reaps every live session (kills ffmpeg, closes its loopback server and removes its segment dir) and halts gcLoop. Called on graceful shutdown so no encoder is left orphaned writing into the cache. Idempotent.
type HLSSessionSnapshot ¶
type HLSSessionSnapshot struct {
Key string `json:"key"`
Codec string `json:"codec"`
SegmentsReady int `json:"segmentsReady"`
StartedAt time.Time `json:"startedAt"`
LastActivity time.Time `json:"lastActivity"`
Pid int `json:"pid"`
}
HLSSessionSnapshot is a read-only representation of an active transcode session.
type HLSStartOpts ¶
type HLSStartOpts struct {
Key string // raw content key, e.g. `${hash}-${fileIdx}`; EffectiveKey may add a mode suffix
Source io.ReadSeeker // seekable input — wrapped by an internal HTTP server
SourceSize int64 // total size hint; required when the underlying reader lies about EOF
VideoCodec string // "h264_nvenc" | "libx264" | etc.
PreserveSourceAudio bool // when true and source audio is AAC, -c:a copy; else transcode to AAC
// NativeHLS marks a Safari/iOS client (native HLS). Combined with the VOD
// policy it decides whether this session uses the finite-VOD path.
NativeHLS bool
// KnownDurationSec lets the caller supply a duration it already probed (the
// local-file path runs ffprobe at play time). >0 skips the in-session 30s
// seekable probe — the rclone/Drive latency win.
KnownDurationSec float64
// ForceVOD opts this session into the finite-VOD (seekbar) path whenever the
// duration is known, BYPASSING the per-client vodMode gate. Used by the
// local-file path: a fully-downloaded file on disk/rclone is complete and
// seekable, so EVENT/live (the last-resort path for unknown-duration
// streams) is wrong for it — VOD is the correct default per the playback
// premise. Torrents leave this false so the global vodMode still guards the
// #61 Safari seek instability on (incomplete) torrent sources.
ForceVOD bool
// AudioOnly transcodes a pure-audio source (FLAC/OGG/Opus/ALAC/WMA/…) to an
// AAC HLS stream with NO video map (`-vn`). The local-file path sets it for
// codecs the target browser can't direct-play (Safari refuses FLAC/OGG/Opus),
// since the video pipeline's unconditional `-map 0:v:0` would fail on a file
// with no video stream.
AudioOnly bool
// AudioTrack é o índice ABSOLUTO da faixa de áudio a mapear no vídeo (>0 =
// escolhida; <=0 = primeira/default). A sessão é keyed pela faixa (ver
// hlsSessionKey) pra que trocar o áudio gere um transcode novo, não reuse o cache.
AudioTrack int
}
HLSStartOpts groups what's needed to spin up a session. The source is the torrent file source; the manager doesn't know about anacrolix specifically.
IMPORTANT: Source MUST implement io.ReadSeeker. We expose it to ffmpeg via an ephemeral loopback HTTP server (one per session) so ffmpeg can issue Range requests and seek freely. Direct pipe-to-stdin (the previous design) fails on MP4 with `moov` at end of file because pipe input is non-seekable — ffmpeg can't walk past a multi-GB mdat box to read the metadata.
type Options ¶
type Options struct {
AudioTrack int // absolute stream index for `-map 0:<n>` (-1 = first audio)
SubBurnTrack int // -1 = none; otherwise absolute stream index for hardsub burn-in
VideoCodec string // "" = copy; "h264" / "hevc" = transcode video
AudioCodec string // "" = copy; "aac" = transcode audio
Container string // "mp4" | "matroska" | "webm" — default "mp4"
SourceVCodec string // optional hint about source video codec (for hwaccel selection)
}
Options describes how to transcode one stream segment. All fields are optional — empty means "keep original / passthrough".
type VODMode ¶
type VODMode int
VODMode gates the finite-VOD (seekbar) HLS path, by client class. See StreamConfig.HLSVODMode. The zero value is VODOff (current/safe behaviour).
func ParseVODMode ¶
ParseVODMode maps the config/env string to a VODMode (default VODOff).