// Package engine describes the playback engines a client can offer and decides // whether a particular file can be direct-played by that client. // // Vellum never transcodes: the server is a headless, GPU-less Xeon, and measured // against real files, playability turned out to be a property of the CLIENT, not // the file. Chrome on Linux plays an MKV carrying H.264 perfectly well, yet // cannot decode an MP4 carrying HEVC at all. So the server's job is only to // report what is inside each file (see internal/probe) and to match it against // what the client says it has. // // This package is the registry of those engines and the matcher. The web app's // only engine is "html5"; native clients (mpv, libvlc, MDK) declare their own. // Adding a native engine later is a registration, not a re-architecture — the // native libraries themselves can only live inside a native client, never in a // browser. package engine import ( "sort" "strings" ) // Engine kinds. A "browser" engine is an HTML5 media element and is bound by // what the browser's decoders support; a "native" engine links a real media // library (mpv/libmpv, libvlc, MDK) and can therefore live only inside a native // client. const ( KindBrowser = "browser" KindNative = "native" ) // HTML5ID is the built-in browser engine every client can be assumed to have. const HTML5ID = "html5" // Engine declares one playback backend and the media it can open. // // The JSON tags are the frozen EngineSupport contract in web/src/lib/api.ts and // must not change without changing that file too. type Engine struct { // ID is the stable identifier clients pass in their declared engine list. ID string `json:"id"` // Name is the human label shown in the UI. Name string `json:"name"` // Kind is KindBrowser or KindNative. Kind string `json:"kind"` // Containers are lowercase extensions, without a leading dot ("mkv"). Containers []string `json:"containers"` // VCodecs are ffprobe video codec names ("h264", "hevc", ...). VCodecs []string `json:"vcodecs"` // ACodecs are ffprobe audio codec names ("aac", "opus", ...). ACodecs []string `json:"acodecs"` } // builtins are the engines Vellum knows out of the box. html5 is the browser // baseline; the native engines are registered so a native client can name them, // and are never selectable from a browser. var builtins = []Engine{ { ID: HTML5ID, Name: "HTML5", Kind: KindBrowser, // A deliberately CONSERVATIVE browser baseline. The real answer comes // from the client: MediaCapabilities (see web/src/lib/capabilities.ts) // can distinguish "cannot decode at all" from "decodes but will // stutter", and only the client's GPU knows that. This list is what the // server can honestly assume of a browser with no other information. // // Note what is absent: hevc/h265 and mpeg4 are measured as undecodable // in Chrome, so they must not be claimed here. Container support is // broad because mkv/mov play fine when the video track is h264. Containers: []string{ "mp4", "m4v", "mov", "mkv", "webm", "mp3", "m4a", "m4b", "aac", "flac", "ogg", "opus", "wav", }, VCodecs: []string{"h264", "vp8", "vp9", "av1"}, // pcm appears under several ffprobe names (pcm_s16le, ...); the aliases // are listed so a WAV is not rejected over a naming detail. ACodecs: []string{"aac", "mp3", "opus", "vorbis", "flac", "pcm", "pcm_s16le", "pcm_s24le", "pcm_f32le", "pcm_u8"}, }, { // mpv (libmpv) handles essentially every ffmpeg demuxer and decoder. // Registered for NATIVE clients only: a browser cannot link libmpv. ID: "mpv", Name: "mpv", Kind: KindNative, Containers: []string{ "mkv", "mp4", "m4v", "mov", "webm", "avi", "wmv", "flv", "ts", "m2ts", "mpg", "mpeg", "ogv", "ogm", "3gp", "vob", }, VCodecs: []string{ "h264", "avc1", "hevc", "h265", "mpeg4", "mpeg2video", "mpeg1video", "vc1", "wmv3", "av1", "vp8", "vp9", "theora", "prores", "dvvideo", }, ACodecs: []string{ "aac", "mp3", "opus", "vorbis", "flac", "ac3", "eac3", "dts", "truehd", "alac", "wma", "pcm", "pcm_s16le", "pcm_s24le", "pcm_f32le", "pcm_u8", }, }, { // libvlc is similarly broad. Registered for NATIVE clients only. ID: "vlc", Name: "VLC", Kind: KindNative, Containers: []string{ "mkv", "mp4", "m4v", "mov", "webm", "avi", "wmv", "flv", "ts", "m2ts", "mpg", "mpeg", "ogv", "ogm", "3gp", "vob", "asf", "rm", }, VCodecs: []string{ "h264", "avc1", "hevc", "h265", "mpeg4", "mpeg2video", "mpeg1video", "vc1", "wmv3", "av1", "vp8", "vp9", "theora", "prores", "dvvideo", "rv40", }, ACodecs: []string{ "aac", "mp3", "opus", "vorbis", "flac", "ac3", "eac3", "dts", "truehd", "alac", "wma", "pcm", "pcm_s16le", "pcm_s24le", "pcm_f32le", "pcm_u8", "cook", }, }, { // MDK is a cross-platform multimedia SDK (ffmpeg-based). Registered for // NATIVE clients only. ID: "mdk", Name: "MDK", Kind: KindNative, Containers: []string{ "mkv", "mp4", "m4v", "mov", "webm", "avi", "wmv", "flv", "ts", "m2ts", "mpg", "mpeg", "ogv", "ogm", "3gp", "vob", }, VCodecs: []string{ "h264", "avc1", "hevc", "h265", "mpeg4", "mpeg2video", "mpeg1video", "vc1", "wmv3", "av1", "vp8", "vp9", "theora", "prores", "dvvideo", }, ACodecs: []string{ "aac", "mp3", "opus", "vorbis", "flac", "ac3", "eac3", "dts", "truehd", "alac", "wma", "pcm", "pcm_s16le", "pcm_s24le", "pcm_f32le", "pcm_u8", }, }, } // registry is built once from builtins. Read-only after init, so All/Get need no // lock; if runtime registration is ever added it must grow a mutex. var registry = func() map[string]Engine { m := make(map[string]Engine, len(builtins)) for _, e := range builtins { m[e.ID] = e } return m }() // All returns every registered engine, sorted by ID so the response is stable. func All() []Engine { out := make([]Engine, 0, len(registry)) for _, e := range registry { out = append(out, e) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out } // Get looks up one engine by ID. The second result is false for an unknown ID, // which callers treat as "this client has no such engine" rather than an error. func Get(id string) (Engine, bool) { e, ok := registry[strings.ToLower(strings.TrimSpace(id))] return e, ok } // Facts packs a file's codec facts into the positional slice Resolve expects: // container, video codec, audio codec. An empty video codec means there is no // video track (audio, or a comic). func Facts(container, vcodec, acodec string) []string { return []string{container, vcodec, acodec} } // canPlay reports whether an engine can direct-play a file with these facts. // // A missing codec ("" for an audio-only file's video track) is not a blocker: // there is simply no such track to decode. Container is always required. func (e Engine) canPlay(container, vcodec, acodec string) bool { if !contains(e.Containers, container) { return false } if vcodec != "" && !contains(e.VCodecs, vcodec) { return false } if acodec != "" && !contains(e.ACodecs, acodec) { return false } return true } // contains reports membership, case-insensitively, so "H264" and "h264" match // and a stray leading dot on a container extension does not. func contains(list []string, want string) bool { want = strings.ToLower(strings.TrimSpace(want)) want = strings.TrimPrefix(want, ".") if want == "" { return false } for _, v := range list { if strings.ToLower(strings.TrimSpace(v)) == want { return true } } return false }