Documentation
¶
Index ¶
- Constants
- Variables
- func ComputeLanguages(audioTracks []AudioTrack) []string
- func CopyVODEligibleCodec(videoCodec string) bool
- func DecodeSubtitleToUTF8(data []byte, langHint string) ([]byte, string)
- func DetectBlackFrameRuns(ctx context.Context, ffmpegPath, mediaPath string, startSec, lengthSec float64, ...) ([]float64, error)
- func DownloadFFmpeg() (string, error)
- func DownloadFFprobe() (string, error)
- func ExtractExternalSubtitleVTT(ctx context.Context, ffmpegPath, subPath, langHint string) ([]byte, error)
- func ExtractFontAttachment(ctx context.Context, ffmpegPath, mediaPath string, index int, filename string) ([]byte, error)
- func ExtractSubtitleASS(ctx context.Context, ffmpegPath, mediaPath string, index int) ([]byte, error)
- func ExtractSubtitleVTT(ctx context.Context, ffmpegPath, mediaPath string, index int) ([]byte, error)
- func ExtractSubtitlesMulti(ctx context.Context, ffmpegPath, mediaPath string, ...) (map[int][]byte, map[int][]byte, error)
- func ExtractSubtitlesVTTMulti(ctx context.Context, ffmpegPath, mediaPath string, indices []int) (map[int][]byte, error)
- func ExtractThumbnailJPEG(ctx context.Context, ffmpegPath, mediaPath string, posSec float64, width int) ([]byte, error)
- func FFmpegCachePath() (string, error)
- func FFprobeCacheDir() (string, error)
- func FFprobeCachePath() (string, error)
- func FilterVTTDrawingCues(vtt []byte) []byte
- func FingerprintAudioWindow(ctx context.Context, ffmpegPath, fpcalcPath, mediaPath string, ...) ([]uint32, error)
- func FontContentType(filename string) string
- func FpcalcCachePath() (string, error)
- func IndexKeyframeWindow(ctx context.Context, ffprobePath, mediaPath string, fromSec, windowSec float64) ([]float64, error)
- func IndexKeyframes(ctx context.Context, ffprobePath, mediaPath string) ([]float64, error)
- func IsASSSubtitleCodec(codec string) bool
- func IsASSSubtitlePath(subPath string) bool
- func IsInconclusiveProbeError(ctx context.Context, err error) bool
- func IsTextSubtitleCodec(codec string) bool
- func LoadAverage1() (float64, bool)
- func LocateFFmpeg(explicit string) (string, bool)
- func LocateFFprobe(explicit string) (string, bool)
- func NormalizeLang(raw string) string
- func PrewarmKeyframes(ctx context.Context, ffprobePath, mediaPath string) error
- func ReadCachedFont(mediaPath string, index int, filename string) ([]byte, bool)
- func ReadCachedKeyframes(mediaPath string) ([]float64, bool)
- func ReadCachedSubtitle(mediaPath string, index int) ([]byte, bool)
- func ReadCachedSubtitleASS(mediaPath string, index int) ([]byte, bool)
- func ReadCachedThumbnail(mediaPath string, posSec float64, width int) ([]byte, bool)
- func ReadExternalSubtitleASS(subPath, langHint string) ([]byte, error)
- func ResolveFFmpeg(explicit string) (string, error)
- func ResolveFFprobe(explicit string) (string, error)
- func ResolveFpcalc() (string, error)
- func SafeFontExt(filename string) string
- func TrickplaySpritePath(mediaPath string, width int) string
- func WriteCachedFont(mediaPath string, index int, filename string, data []byte) error
- func WriteCachedKeyframes(mediaPath string, kfs []float64) error
- func WriteCachedSkipSegments(mediaPath string, durationSec float64, segs []SkipSegmentRange) error
- func WriteCachedSubtitle(mediaPath string, index int, vtt []byte) error
- func WriteCachedSubtitleASS(mediaPath string, index int, ass []byte) error
- func WriteCachedThumbnail(mediaPath string, posSec float64, width int, jpeg []byte) error
- type AudioTrack
- type FontAttachment
- type IntegrityInfo
- type MediaInfo
- type SharedRegion
- type SkipSegmentRange
- type SkipSegmentsSidecar
- type SubtitleTrack
- type TrickplayManifest
- type VideoInfo
Constants ¶
const (
// ChromaprintSampleDur is seconds of audio per fingerprint point.
ChromaprintSampleDur = 0.1238
)
Variables ¶
var ErrKeyframeIndexTimeout = errors.New("keyframe index timed out")
ErrKeyframeIndexTimeout marks an index aborted by its own deadline rather than by a real demux failure. Callers distinguish the two because they mean opposite things: a timeout says "too slow here, try again later / prewarm it", while a demux error says "this file will never index". Mirrors ErrProbeExpired.
var ErrNotStyledSubtitle = errors.New("subtitle stream carries no ASS styling")
ErrNotStyledSubtitle means the extracted script carries no authored styling, so serving it as an ASS original would be a lie. Callers fall back to WebVTT.
var ErrProbeExpired = errors.New("probe expired")
ErrProbeExpired marks a deep probe that ran out of time rather than reaching a verdict. It is strictly a scheduling signal: the file is NOT damaged and NOT known-healthy — it simply was not checked, so the caller can re-queue it under less pressure. Never map this to a damaged verdict.
var ErrProbeInconclusive = errors.New("probe inconclusive")
ErrProbeInconclusive wraps a probe failure that says nothing about whether the FILE is healthy: the context was cancelled, ffprobe timed out, was killed (OOM / signal), never started, or the path was momentarily unreachable (a network share blip). Callers must NOT report these as a damaged file — see IsInconclusiveProbeError.
var ErrTrickplayInProgress = errors.New("trickplay: generation already in progress")
ErrTrickplayInProgress means another worker — possibly an agent on another host sharing the same library (e.g. the dev binary on /mnt/nas and the docker agent on /downloads, the SAME files) — already holds this sprite's lock and is generating it. The caller must SKIP, not count it as a failure.
Functions ¶
func ComputeLanguages ¶
func ComputeLanguages(audioTracks []AudioTrack) []string
ComputeLanguages extracts unique ISO 639-1 language codes from audio tracks.
func CopyVODEligibleCodec ¶
CopyVODEligibleCodec reports whether a video codec can ride COPY-VOD's MPEG-TS transport (H.264 only): TS carries it universally; HEVC needs fMP4 (Apple HLS) and AV1 isn't a TS codec. Placed in this leaf package so the stream engine (segment planning) and the scan-time prewarm (which items to keyframe-index) classify codecs identically — same rationale as IsTextSubtitleCodec.
func DecodeSubtitleToUTF8 ¶
DecodeSubtitleToUTF8 returns the bytes as UTF-8, transcoding from a detected legacy encoding when needed. The returned name is for logging ("utf-8", "bom-utf16le", "windows-1256", …). Never fails: a transcode error falls back to the original bytes (ffmpeg may still cope).
func DetectBlackFrameRuns ¶
func DetectBlackFrameRuns(ctx context.Context, ffmpegPath, mediaPath string, startSec, lengthSec float64, minBlackPct int) ([]float64, error)
DetectBlackFrameRuns scans [startSec, startSec+lengthSec] with ffmpeg's blackframe filter and returns the timestamps (absolute seconds) of frames that are ≥minBlackPct black. Used to find the start of end credits in movies (classic credits roll on black).
func DownloadFFmpeg ¶
DownloadFFmpeg downloads a static ffmpeg binary for the current platform and caches it locally. Returns the path to the binary. Reuses resolveFFprobeURL's ffbinaries.com discovery endpoint — that index ships both ffprobe and ffmpeg per platform.
func DownloadFFprobe ¶
DownloadFFprobe downloads a static ffprobe binary for the current platform and caches it locally. Returns the path to the binary.
func ExtractExternalSubtitleVTT ¶
func ExtractExternalSubtitleVTT(ctx context.Context, ffmpegPath, subPath, langHint string) ([]byte, error)
ExtractExternalSubtitleVTT converts a STANDALONE sidecar subtitle file (a .srt/.ass/.ssa/.vtt sitting next to the media) to WebVTT. Unlike the embedded path it has no stream index — the whole file is the track. It first transcodes the bytes to UTF-8 (legacy code pages → mojibake otherwise; see charset.go) using the track's language as the detection hint, then runs ffmpeg to emit WebVTT. The UTF-8 bytes go through a temp file with the ORIGINAL extension so ffmpeg selects the right demuxer (.srt→subrip, .ass→ass, .vtt→webvtt), and `-sub_charenc UTF-8` stops ffmpeg from re-guessing what we already decoded.
func ExtractFontAttachment ¶ added in v1.11.0
func ExtractFontAttachment(ctx context.Context, ffmpegPath, mediaPath string, index int, filename string) ([]byte, error)
ExtractFontAttachment dumps font attachment `index` (ffmpeg's t:N ordering, see FontAttachment.Index) out of mediaPath and returns its bytes.
ffmpeg has no "dump to stdout" mode for attachments, so this writes to a temp file and reads it back. It also EXITS NON-ZERO on success here: -dump_attachment does its work during input parsing and then ffmpeg complains "At least one output file must be specified" because the command specifies no output. The error is therefore only fatal when the dump produced nothing — verified against real MKVs, where the font lands on disk regardless of the exit code.
func ExtractSubtitleASS ¶ added in v1.11.0
func ExtractSubtitleASS(ctx context.Context, ffmpegPath, mediaPath string, index int) ([]byte, error)
ExtractSubtitleASS runs ffmpeg to copy subtitle stream `index` of mediaPath out as a raw ASS/SSA script.
`-f ass` on a NON-ass source (subrip, mov_text) must DECLINE, not fabricate: the URL layer only advertises assUrl for ass/ssa tracks, but the /sub token deliberately does not bind the serialisation, so anyone holding a token for a subrip track can still ask for `f=ass`. How ffmpeg reacts depends on version: ffmpeg 8's ass muxer refuses `-c:s copy` of a non-ass codec outright ("ass muxer supports only codec ass"), which is mapped to ErrNotStyledSubtitle below; older ffmpeg synthesised a lone "Default" style when TRANSCODING, so the OUTPUT is additionally checked: a genuine ASS original carries an authored style table (see hasAuthoredStyles). The content check doubles as the guard for an exotic mux whose extradata failed to round-trip.
The caller owns the ctx deadline, as with ExtractSubtitleVTT.
func ExtractSubtitleVTT ¶
func ExtractSubtitleVTT(ctx context.Context, ffmpegPath, mediaPath string, index int) ([]byte, error)
ExtractSubtitleVTT runs ffmpeg to convert subtitle stream `index` of mediaPath to WebVTT bytes. Shared by the on-demand /sub handler and the scan-time prewarm so both produce identical output. The caller owns the ctx deadline: the handler uses a short HTTP-bound timeout; the prewarm uses a generous one (a full text track on a multi-GB remux can take minutes to demux).
func ExtractSubtitlesMulti ¶ added in v1.11.0
func ExtractSubtitlesMulti(ctx context.Context, ffmpegPath, mediaPath string, vttIndices, assIndices []int) (map[int][]byte, map[int][]byte, error)
ExtractSubtitlesMulti extracts WebVTT conversions for vttIndices AND raw ASS/SSA copies for assIndices from mediaPath in ONE ffmpeg pass. Subtitle packets are interleaved across the whole container, so extraction is I/O-bound on a single sequential read of the file — emitting the .ass sidecars in the same pass makes the ass prewarm free where a second pass would re-read a multi-GB remux end to end.
assIndices MUST contain only ass/ssa-codec streams (IsASSSubtitleCodec): ffmpeg 8's ass muxer refuses `-c:s copy` of any other codec at header-write time, which would abort the WHOLE command, VTT outputs included. As a belt-and-braces guard the function retries VTT-only if the combined pass fails cleanly and assIndices was non-empty.
func ExtractSubtitlesVTTMulti ¶
func ExtractSubtitlesVTTMulti(ctx context.Context, ffmpegPath, mediaPath string, indices []int) (map[int][]byte, error)
ExtractSubtitlesVTTMulti extracts several text subtitle streams in a SINGLE ffmpeg pass. The expensive part of subtitle extraction is demuxing the whole container (subtitle packets are interleaved across the runtime), so a 60GB remux with N text tracks costs N full reads when done one index at a time — here it's one read for all of them. Returns index→WebVTT for the streams that produced output (an empty stream is simply absent, not an error). ffmpeg can't multiplex several outputs onto stdout, so it writes per-track temp files which are read back; callers cache them via WriteCachedSubtitle.
func ExtractThumbnailJPEG ¶
func ExtractThumbnailJPEG(ctx context.Context, ffmpegPath, mediaPath string, posSec float64, width int) ([]byte, error)
ExtractThumbnailJPEG decodes ONE frame at posSec, scaled to `width`, as JPEG bytes. The fast path mirrors engine.buildThumbnailArgs (`-ss` before `-i` = fast input/keyframe seek); on a seek-index failure both this prewarm path and the on-demand handler fall back to the identical output-seek argv (thumbnailArgsAccurate / engine.buildThumbnailArgsAccurate), so the two stay equivalent in both paths. Shared by the prewarm; the handler keeps its own inline extraction (engine package) and only reuses the cache helpers here.
func FFmpegCachePath ¶
FFmpegCachePath returns the full path to the cached ffmpeg binary (sibling of the cached ffprobe binary).
func FFprobeCacheDir ¶
FFprobeCacheDir returns the directory where the downloaded ffprobe binary is stored.
func FFprobeCachePath ¶
FFprobeCachePath returns the full path to the cached ffprobe binary.
func FilterVTTDrawingCues ¶ added in v1.11.0
FilterVTTDrawingCues removes cues whose text is an ASS vector-drawing path leaked into WebVTT by ffmpeg's ass→webvtt converter (see the file comment). Everything else — the WEBVTT header, NOTE/STYLE/REGION blocks, cue settings, timings and ordinary cue text — is passed through byte-for-byte.
Input that contains no such cue is returned unchanged (same backing array), so this is safe to run on every subtitle response regardless of the source format. When something IS dropped the output is re-emitted with uniform line endings — see the note in the body.
func FingerprintAudioWindow ¶
func FingerprintAudioWindow(ctx context.Context, ffmpegPath, fpcalcPath, mediaPath string, startSec, lengthSec float64) ([]uint32, error)
FingerprintAudioWindow decodes [startSec, startSec+lengthSec] of the first audio track with ffmpeg and pipes the WAV into fpcalc -raw, returning the chromaprint point stream.
func FontContentType ¶ added in v1.11.0
FontContentType maps a font filename to the MIME type to serve it as. Browsers do not sniff fonts, and libass only needs the bytes, but a correct type keeps devtools honest and avoids any chance of a security heuristic rejecting the response.
func FpcalcCachePath ¶
FpcalcCachePath returns the cached fpcalc binary path (same bin dir as the downloaded ffmpeg/ffprobe).
func IndexKeyframeWindow ¶
func IndexKeyframeWindow(ctx context.Context, ffprobePath, mediaPath string, fromSec, windowSec float64) ([]float64, error)
IndexKeyframeWindow indexes ONLY the keyframes in [fromSec, fromSec+windowSec) via ffprobe's `-read_intervals`, so a cold start can plan segments around the user's resume point without paying the full-file demux (measured over NFS: 7 s for a 300 s window vs 252 s for the whole 15 GB file). The timestamps it returns are identical to the corresponding slice of a full index — verified against one — so a window is a true subset, never an approximation.
fromSec <= 0 windows from the start of the file.
func IndexKeyframes ¶
IndexKeyframes returns the sorted presentation timestamps (seconds) of every video keyframe in mediaPath.
It reads PACKET headers (`-show_entries packet=pts_time,flags`) and keeps the ones flagged keyframe ("K") — a demux-only pass, NOT a decode. This is ~40× faster than `-skip_frame nokey` (which decodes each keyframe). Still a full demux of the container, hence local-file only. Errors if no keyframes are found.
func IsASSSubtitleCodec ¶ added in v1.11.0
IsASSSubtitleCodec reports whether a probed subtitle codec is ASS/SSA — the only codecs whose original script `-c:s copy -f ass` can round-trip. Shared by the scan-time prewarm and the engine's URL layer so both classify identically.
func IsASSSubtitlePath ¶ added in v1.11.0
IsASSSubtitlePath reports whether an EXTERNAL sidecar file is an ASS/SSA script, by extension. The extension is the honest signal here: sidecars are author-named files, and serving a .srt's bytes under `f=ass` would hand a libass client an unparseable "script" (200 + no subtitles, no error).
func IsInconclusiveProbeError ¶
IsInconclusiveProbeError reports whether a probe error is inconclusive about the file's integrity — either explicitly wrapped as ErrProbeInconclusive, or because the caller's own context is already done (a scan cancelled between the probe returning and this check).
func IsTextSubtitleCodec ¶
IsTextSubtitleCodec reports whether a subtitle codec can be extracted to WebVTT (text-based). Mirrors engine.ProbeSubtitleTrack.IsTextSubtitle and the web's isTextSubtitleCodec whitelist — bitmap subs (PGS/DVB/VOBSUB) are burned in, not extracted. Defined here (the leaf media package) so both the stream handlers and the scan-time prewarm classify codecs identically.
func LoadAverage1 ¶
LoadAverage1 returns the 1-minute system load from /proc/loadavg. ok=false when it can't be read, so callers treat "unknown" as "don't gate" (proceed) rather than blocking forever.
func LocateFFmpeg ¶
LocateFFmpeg finds an ffmpeg that is ALREADY on this host and never downloads one. That distinction is the whole point of the split from ResolveFFmpeg: `unarr doctor` has to answer "is ffmpeg installed?" in milliseconds, and ResolveFFmpeg answers it by fetching ~50 MB — which would turn an interactive diagnostic into a silent installer and report "present" for a host that had nothing a second earlier.
Search order (same as ResolveFFmpeg, minus the download):
- explicit path (--ffmpeg flag / [library] ffmpeg_path)
- FFMPEG_PATH env var
- "ffmpeg" on PATH
- adjacent to the running executable (release tarballs bundle it there)
- a copy downloaded by an earlier run, in the unarr cache dir
func LocateFFprobe ¶
LocateFFprobe is LocateFFmpeg for ffprobe. See its doc comment.
func NormalizeLang ¶
NormalizeLang converts a language code to ISO 639-1. Returns "und" for empty input, the input lowercased if no mapping is found.
func PrewarmKeyframes ¶
PrewarmKeyframes indexes + caches the keyframe table for mediaPath unless a fresh sidecar already exists. Best-effort, idempotent — the scan-time prewarm job. Returns nil (no work) when the cache is already fresh.
func ReadCachedFont ¶ added in v1.11.0
ReadCachedFont returns the cached font attachment for (mediaPath, index).
func ReadCachedKeyframes ¶
ReadCachedKeyframes returns the cached keyframe index when a fresh sidecar exists. ok=false means the caller should IndexKeyframes on demand.
func ReadCachedSubtitle ¶
ReadCachedSubtitle returns the cached WebVTT for (mediaPath, index) when a fresh sidecar exists. ok=false means the caller should extract on demand.
func ReadCachedSubtitleASS ¶ added in v1.11.0
ReadCachedSubtitleASS returns the cached raw .ass for (mediaPath, index) when a fresh sidecar exists. ok=false means the caller should extract on demand.
func ReadCachedThumbnail ¶
ReadCachedThumbnail returns the cached JPEG for (mediaPath, posSec, width) when a fresh sidecar exists. ok=false means extract on demand.
func ReadExternalSubtitleASS ¶ added in v1.11.0
ReadExternalSubtitleASS returns a standalone .ass/.ssa sidecar as UTF-8 bytes.
No ffmpeg: the file already IS the format we want to serve, so running it through the muxer would only risk losing sections it does not round-trip. The charset transcode still applies — fansub .ass files predating UTF-8 ubiquity are common, and libass would render mojibake otherwise.
func ResolveFFmpeg ¶
ResolveFFmpeg finds the ffmpeg binary. Search order mirrors ResolveFFprobe so the same operator setup works for both:
- Explicit path (--ffmpeg flag / library.ffmpeg_path config)
- FFMPEG_PATH env var
- "ffmpeg" on PATH
- Adjacent to the current executable (release tarball bundles ffmpeg next to the unarr binary — this is the preferred install path)
- Previously downloaded in the unarr cache dir
- Auto-download static binary as last resort (~50MB, slow start)
ffmpeg is required for the HLS streaming pipeline; ffprobe alone can't transcode HEVC/MKV to browser-friendly H.264/MP4 fragments.
func ResolveFFprobe ¶
ResolveFFprobe finds the ffprobe binary. Search order: 1. Explicit path (--ffprobe flag) 2. FFPROBE_PATH env var 3. "ffprobe" in PATH 4. Adjacent to the current executable 5. Previously downloaded in cache dir 6. Auto-download static binary
func ResolveFpcalc ¶
ResolveFpcalc finds a usable fpcalc binary: PATH → cache dir → download.
func SafeFontExt ¶ added in v1.11.0
isFontAttachment reports whether a container attachment is a font, and so worth extracting for an .ass renderer. Either signal is enough: a recognised mimetype, or a recognised filename extension.
Non-font attachments (cover art, chapter XML, the odd README) are excluded — but note they still advance FontAttachment.Index, since ffmpeg's -dump_attachment:t:N counts every attachment stream. SafeFontExt returns the cache-file extension to use for an attachment filename, constrained to the known font extensions.
The filename reaches us as an untrusted query parameter. filepath.Ext already prevents traversal (only the extension survives), but without this whitelist `n=x.php` or `n=a.<svg onload=…>` would create a correspondingly-named file inside the user's .unarr/ cache directory — contained, but no reason to allow.
func TrickplaySpritePath ¶
TrickplaySpritePath is the public accessor the stream server uses to locate the cached sprite JPEG for serving.
func WriteCachedFont ¶ added in v1.11.0
WriteCachedFont stores a dumped font next to the media. Best-effort.
func WriteCachedKeyframes ¶
WriteCachedKeyframes stores the keyframe index next to the media. Best-effort (a read-only mount just means no cache; the on-demand index still works).
func WriteCachedSkipSegments ¶
func WriteCachedSkipSegments(mediaPath string, durationSec float64, segs []SkipSegmentRange) error
WriteCachedSkipSegments persists a detection result next to the media file.
func WriteCachedSubtitle ¶
WriteCachedSubtitle stores extracted WebVTT next to the media. Best-effort.
func WriteCachedSubtitleASS ¶ added in v1.11.0
WriteCachedSubtitleASS stores extracted .ass next to the media. Best-effort.
Types ¶
type AudioTrack ¶
type AudioTrack struct {
Lang string `json:"lang"` // ISO 639-1
Codec string `json:"codec"` // "aac", "ac3", "dts", "truehd"
Channels int `json:"channels"` // 2, 6, 8
Title string `json:"title"`
Default bool `json:"default"`
}
AudioTrack represents a single audio stream.
type FontAttachment ¶ added in v1.11.0
type FontAttachment struct {
// Index addresses the attachment for `ffmpeg -dump_attachment:t:<Index>`.
//
// It is the position among ATTACHMENT streams — NOT the global stream index,
// and NOT the position after filtering to fonts. On a real release those
// differ wildly: Skeleton Knight S01E01 has attachments t:0..t:25 whose
// stream indices run 16..41. Getting this wrong silently dumps the wrong
// file, so the counter must advance for EVERY attachment, font or not.
Index int `json:"index"`
Filename string `json:"filename"`
Mimetype string `json:"mimetype,omitempty"`
}
FontAttachment is a font file muxed into the container, needed to render an .ass subtitle the way its author typeset it.
type IntegrityInfo ¶
type IntegrityInfo struct {
Damaged bool `json:"damaged"`
Reason string `json:"reason,omitempty"`
// Unverified marks a file the deep probe never managed to check (its tail
// demux ran past truncProbeTimeout on slow/contended storage) even after the
// deferred serial retry. It is NOT a verdict: such a file is neither known
// healthy nor damaged, and Damaged stays false, so it can never be synced as
// corrupt. It exists so "checked and clean" stops being indistinguishable
// from "we never got to look" — before this, both were a plain nil verdict
// and nothing upstream could tell a user what was still unverified.
Unverified bool `json:"unverified,omitempty"`
}
IntegrityInfo flags a file whose metadata probed OK enough to land in the library but that shows structural damage — the hallmark of an incomplete or corrupt download. Reason is a stable code the web localizes; two families:
header probe (assessIntegrity): "invalid_data", "ebml_corrupt", "moov_missing", "bitstream_corrupt", "no_duration". deep probe (AssessTruncation): "truncated" (tail data stops before the header's claimed duration), "tail_corrupt" (bytes short but tail decode fails).
func AssessTruncation ¶
func AssessTruncation(ctx context.Context, ffprobePath, ffmpegPath, filePath string, headerDur float64) (*IntegrityInfo, error)
AssessTruncation runs the deep (post-header) truncation checks on a LOCAL video file whose header already probed OK. Returns a "damaged" verdict only on a corroborated signal, else nil. headerDur is the container's claimed duration (seconds); ffmpegPath may be "" → the decode confirm (C) is skipped.
Conservative by construction: it only flags when the file's own header contradicts its contents, so a healthy file is never marked damaged.
The error return is ONLY a scheduling signal, never a verdict: ErrProbeExpired means the tail demux ran out of time (slow/contended storage) so the file was left UNCHECKED and deserves a deferred retry. Callers that ignore the error keep exactly the old behaviour — nil verdict, nothing flagged.
type MediaInfo ¶
type MediaInfo struct {
Video *VideoInfo `json:"video"`
Audio []AudioTrack `json:"audio"`
Subtitles []SubtitleTrack `json:"subtitles"`
Languages []string `json:"languages"` // derived from audio tracks
// Fonts are the font files muxed into the container as attachments. Fansub
// .ass tracks name fonts the viewer's machine almost never has, so a faithful
// render needs these shipped alongside the subtitle. Empty for the vast
// majority of files — only anime/fansub releases carry them.
Fonts []FontAttachment `json:"fonts,omitempty"`
// Integrity is non-nil only when the scan found signs of corruption / an
// incomplete download. Surfaced in the web library as a "damaged" warning
// so the user re-downloads instead of hitting a file that won't play.
Integrity *IntegrityInfo `json:"integrity,omitempty"`
}
MediaInfo holds the media analysis result from ffprobe.
type SharedRegion ¶
type SharedRegion struct {
SharedRegion is the longest aligned similar-audio region between two fingerprint streams, in seconds relative to each stream's start.
func FindSharedRegion ¶
func FindSharedRegion(a, b []uint32, minDur, maxDur float64) *SharedRegion
FindSharedRegion locates the longest contiguous region (bounded by minDur/maxDur seconds) where streams a and b carry near-identical audio at some alignment. Returns nil when no qualifying region exists.
type SkipSegmentRange ¶
type SkipSegmentRange struct {
Category string `json:"category"` // "intro" | "credits"
StartSec float64 `json:"startSec"`
EndSec float64 `json:"endSec"`
}
SkipSegmentRange is one detected skippable range inside a media file.
type SkipSegmentsSidecar ¶
type SkipSegmentsSidecar struct {
Version int `json:"version"`
DurationSec float64 `json:"durationSec"`
Segments []SkipSegmentRange `json:"segments"` // empty = analyzed, nothing found
}
SkipSegmentsSidecar is the cached detection result for one media file.
func ReadCachedSkipSegments ¶
func ReadCachedSkipSegments(mediaPath string) (*SkipSegmentsSidecar, bool)
ReadCachedSkipSegments returns the cached detection result for mediaPath if fresh (newer than the media file) and of the current algorithm version.
type SubtitleTrack ¶
type SubtitleTrack struct {
Lang string `json:"lang"`
Codec string `json:"codec"`
Title string `json:"title"`
Forced bool `json:"forced"`
// External is true for a sidecar file; false (omitted) for an embedded stream.
External bool `json:"external,omitempty"`
// Path is the absolute filesystem path of the sidecar file (External only).
// Empty for embedded streams (those live inside the media container).
Path string `json:"path,omitempty"`
}
SubtitleTrack represents a single subtitle source — either an EMBEDDED stream (the common case, identified by its ffmpeg `0:s:N` order in the slice) or an EXTERNAL sidecar file sitting next to the media (Path set, External true).
External sidecars (a `.srt`/`.ass`/`.vtt` named after the video, or one in a `Subs/` subfolder) are appended AFTER all embedded tracks so the embedded tracks keep slice positions equal to their `0:s:N` index — the web's resolveSubtitleTracks relies on that for embedded, and switches to Path-based addressing for external (served via /sub?p=<file>&i=-1).
func DiscoverSidecarSubtitles ¶
func DiscoverSidecarSubtitles(mediaPath string) []SubtitleTrack
DiscoverSidecarSubtitles finds external subtitle files for a local media file: siblings named after the video, plus everything in a Subs/Subtitles subfolder. Returns text tracks only, each with External=true and an absolute Path. Safe on any path — returns nil if the directory can't be read (best-effort, like the rest of the scan). Never call for a remote URL source (no local directory).
NOTE: discovered sidecars are NOT deduped against embedded streams of the same language. That's deliberate — a `Movie.en.srt` next to a video that also has an embedded English stream is usually a DIFFERENT track (full vs SDH, retimed, or a better translation), so silently dropping either would hide a choice the user may want. Both surface as separate, distinctly-labelled entries.
type TrickplayManifest ¶
type TrickplayManifest struct {
Version int `json:"version"` // schema version (1)
IntervalSec float64 `json:"intervalSec"`
TileWidth int `json:"tileWidth"`
TileHeight int `json:"tileHeight"`
Cols int `json:"cols"`
Rows int `json:"rows"`
Count int `json:"count"` // number of REAL frames (≤ Cols*Rows; the rest are padding)
DurationSec float64 `json:"durationSec"`
}
TrickplayManifest describes the montage sprite layout so a client can map a playback time to one tile: tileIndex = floor(timeSec / IntervalSec), then col = tileIndex % Cols, row = tileIndex / Cols, and the tile's pixel box is (col*TileWidth, row*TileHeight, TileWidth, TileHeight).
func GenerateTrickplay ¶
func GenerateTrickplay(ctx context.Context, ffmpegPath, mediaPath string, intervalSec float64, width int, durationSec float64) (TrickplayManifest, error)
GenerateTrickplay builds the montage sprite + manifest for mediaPath and caches them in the sidecar dir. ONE ffmpeg pass samples a frame every intervalSec (fps=1/interval), scales each to width (even height), and tiles them into a single JPEG.
`-skip_frame nokey` makes the decoder touch ONLY keyframes — ~12× less CPU than the old full decode (measured 233 s → 19 s CPU on a 24-min 1080p episode), which matters because this runs alongside live streaming on the same box. The fps filter still emits one frame per UNIFORM tick (it repeats the latest keyframe for ticks between keyframes), so the manifest contract — tileIndex = floor(t / IntervalSec) — is unchanged and cached clients keep working; each tile just shows the nearest keyframe ≤ its tick (≤ one GOP off, invisible at 240-320 px scrub size).
durationSec drives the grid size; pass the probed duration (0 → error, nothing to sample). The caller owns the ctx deadline (generous at scan time).
func ReadCachedTrickplay ¶
func ReadCachedTrickplay(mediaPath string, width int) (TrickplayManifest, bool)
ReadCachedTrickplay returns the manifest when a fresh sprite + manifest exist for (mediaPath, width). ok=false means the caller should (re)generate. Both the sprite and the manifest must be at least as new as the media file.
type VideoInfo ¶
type VideoInfo struct {
Codec string `json:"codec"` // "hevc", "h264", "av1"
Width int `json:"width"`
Height int `json:"height"`
BitDepth int `json:"bitDepth"` // 8, 10, 12
HDR string `json:"hdr"` // "HDR10", "DV", "HLG", "DV+HDR10", ""
FrameRate float64 `json:"frameRate"` // e.g. 23.976
Profile string `json:"profile"` // e.g. "Main 10", "High"
Duration float64 `json:"duration"` // seconds
}
VideoInfo represents the primary video stream metadata.