mediainfo

package
v1.11.3 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// ChromaprintSampleDur is seconds of audio per fingerprint point.
	ChromaprintSampleDur = 0.1238
)

Variables

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

func CopyVODEligibleCodec(videoCodec string) bool

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

func DecodeSubtitleToUTF8(data []byte, langHint string) ([]byte, string)

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

func DownloadFFmpeg() (string, error)

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

func DownloadFFprobe() (string, error)

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

func FFmpegCachePath() (string, error)

FFmpegCachePath returns the full path to the cached ffmpeg binary (sibling of the cached ffprobe binary).

func FFprobeCacheDir

func FFprobeCacheDir() (string, error)

FFprobeCacheDir returns the directory where the downloaded ffprobe binary is stored.

func FFprobeCachePath

func FFprobeCachePath() (string, error)

FFprobeCachePath returns the full path to the cached ffprobe binary.

func FilterVTTDrawingCues added in v1.11.0

func FilterVTTDrawingCues(vtt []byte) []byte

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

func FontContentType(filename string) string

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

func FpcalcCachePath() (string, error)

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

func IndexKeyframes(ctx context.Context, ffprobePath, mediaPath string) ([]float64, error)

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

func IsASSSubtitleCodec(codec string) bool

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

func IsASSSubtitlePath(subPath string) bool

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

func IsInconclusiveProbeError(ctx context.Context, err error) bool

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

func IsTextSubtitleCodec(codec string) bool

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

func LoadAverage1() (float64, bool)

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

func LocateFFmpeg(explicit string) (string, bool)

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):

  1. explicit path (--ffmpeg flag / [library] ffmpeg_path)
  2. FFMPEG_PATH env var
  3. "ffmpeg" on PATH
  4. adjacent to the running executable (release tarballs bundle it there)
  5. a copy downloaded by an earlier run, in the unarr cache dir

func LocateFFprobe

func LocateFFprobe(explicit string) (string, bool)

LocateFFprobe is LocateFFmpeg for ffprobe. See its doc comment.

func NormalizeLang

func NormalizeLang(raw string) string

NormalizeLang converts a language code to ISO 639-1. Returns "und" for empty input, the input lowercased if no mapping is found.

func PrewarmKeyframes

func PrewarmKeyframes(ctx context.Context, ffprobePath, mediaPath string) error

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

func ReadCachedFont(mediaPath string, index int, filename string) ([]byte, bool)

ReadCachedFont returns the cached font attachment for (mediaPath, index).

func ReadCachedKeyframes

func ReadCachedKeyframes(mediaPath string) ([]float64, bool)

ReadCachedKeyframes returns the cached keyframe index when a fresh sidecar exists. ok=false means the caller should IndexKeyframes on demand.

func ReadCachedSubtitle

func ReadCachedSubtitle(mediaPath string, index int) ([]byte, bool)

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

func ReadCachedSubtitleASS(mediaPath string, index int) ([]byte, bool)

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

func ReadCachedThumbnail(mediaPath string, posSec float64, width int) ([]byte, bool)

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

func ReadExternalSubtitleASS(subPath, langHint string) ([]byte, error)

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

func ResolveFFmpeg(explicit string) (string, error)

ResolveFFmpeg finds the ffmpeg binary. Search order mirrors ResolveFFprobe so the same operator setup works for both:

  1. Explicit path (--ffmpeg flag / library.ffmpeg_path config)
  2. FFMPEG_PATH env var
  3. "ffmpeg" on PATH
  4. Adjacent to the current executable (release tarball bundles ffmpeg next to the unarr binary — this is the preferred install path)
  5. Previously downloaded in the unarr cache dir
  6. 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

func ResolveFFprobe(explicit string) (string, error)

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

func ResolveFpcalc() (string, error)

ResolveFpcalc finds a usable fpcalc binary: PATH → cache dir → download.

func SafeFontExt added in v1.11.0

func SafeFontExt(filename string) string

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

func TrickplaySpritePath(mediaPath string, width int) string

TrickplaySpritePath is the public accessor the stream server uses to locate the cached sprite JPEG for serving.

func WriteCachedFont added in v1.11.0

func WriteCachedFont(mediaPath string, index int, filename string, data []byte) error

WriteCachedFont stores a dumped font next to the media. Best-effort.

func WriteCachedKeyframes

func WriteCachedKeyframes(mediaPath string, kfs []float64) error

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

func WriteCachedSubtitle(mediaPath string, index int, vtt []byte) error

WriteCachedSubtitle stores extracted WebVTT next to the media. Best-effort.

func WriteCachedSubtitleASS added in v1.11.0

func WriteCachedSubtitleASS(mediaPath string, index int, ass []byte) error

WriteCachedSubtitleASS stores extracted .ass next to the media. Best-effort.

func WriteCachedThumbnail

func WriteCachedThumbnail(mediaPath string, posSec float64, width int, jpeg []byte) error

WriteCachedThumbnail stores an extracted JPEG frame 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.

func ExtractMediaInfo

func ExtractMediaInfo(ctx context.Context, ffprobePath, filePath string) (*MediaInfo, error)

ExtractMediaInfo runs ffprobe on a file and parses audio, subtitle, and video streams.

type SharedRegion

type SharedRegion struct {
	AStart, AEnd float64
	BStart, BEnd float64
	Duration     float64
}

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.

Jump to

Keyboard shortcuts

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