Local files, direct media URLs and streams
vid2context reads more than platform links: recordings on disk, direct links to .mp3 or .mp4 files, and HLS or DASH streams. This guide covers what works, how subtitles that come with a file are used, and how ffmpeg is found.
npm run cli -- ./talk.mp4
npm run cli -- ~/Recordings/interview.m4a --timestamps
npm run cli -- https://upload.wikimedia.org/wikipedia/commons/5/5a/Discovery_-_Nice_to_be_in_orbit.mp3
npm run cli -- https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8Supported inputs
| Input | Provider | Runtimes | Notes |
|---|---|---|---|
A local path (./talk.mp4, /abs/path.mkv, ~/a.mp3) or file:// URL |
media-file |
Node | Extensions: .mp3 .mpga .mpeg .m4a .aac .wav .flac .ogg .oga .opus .weba .webm .mp4 .m4v .mov .mkv .avi. A path that starts with ./, / or ~/ matches whatever its extension. |
| An http(s) link whose path ends in a media extension | media-url |
Node, Workers, browser | .mp3 .m4a .aac .wav .flac .ogg .opus .mp4 .m4v .webm .mov .mkv; query strings are ignored for matching. |
An HLS .m3u8 or DASH .mpd URL |
media-stream |
Node | On-demand streams only; live streams are refused. |
Ref ids: local files are file:<sha256 of path, size and mtime>, so editing the file gives a new id and never serves a stale cache. URLs are media:<sha256 of the URL without its fragment>.
What you get
For local files with ffmpeg installed, metadata comes from the container: the title tag (or the file name), artist (as the channel), date, comment, duration, container chapters, and the spoken language when the audio stream is tagged. extra.streams lists the streams.
Audio for speech-to-text:
- Video files: the first audio track is extracted as 16 kHz mono mp3 at 32 kbps, about 14 MB per hour. That keeps roughly 1.7 hours under the 25 MB upload limit of Groq and OpenAI; longer audio is split by the long-audio orchestrator.
- Audio files up to 25 MB in a format speech-to-text accepts (
mp3 m4a wav flac ogg opus) go through untouched. Larger or other formats are re-encoded like video.
Subtitles that come with a file
Before transcribing, media-file-captions looks for subtitles. When it finds usable ones, the transcript is creator-captions and no speech-to-text runs (check attempts: there is no audio or stt stage).
Sidecar files sit next to the media and share its name:
File next to talk.mp4 |
Language |
|---|---|
talk.srt, talk.vtt |
the audio stream's tag, else und |
talk.en.srt, talk.eng.vtt |
en |
talk.pt-BR.srt |
pt-BR |
talk.en.forced.srt |
listed, never used as the transcript (forced tracks hold only foreign-language lines) |
talk.final.srt |
ignored: it may belong to talk.final.mp4 |
Modifiers sdh, cc and default are allowed in the name. Parsing handles a UTF-8 BOM, CRLF line endings, missing cue numbers, cue settings, multi-line cues, MM:SS timings, HTML-style tags, ASS override codes and WebVTT voice tags (kept as the speaker).
Embedded subtitle streams (mkv subrip or ass, mp4 mov_text, webvtt) are read with ffmpeg when there is no sidecar. Image-based subtitles (DVD, Blu-ray PGS) hold pictures, not text, and are skipped.
Order: sidecars before embedded streams, untagged sidecars first. With --lang xx, tracks in that language come first, then untagged ones; if only other languages exist, speech-to-text runs instead. The transcript's model field names the track used, for example sidecar:talk.en.srt or embedded:2.
ffmpeg
@vid2context/ffmpeg finds ffmpeg and ffprobe in this order:
FFMPEG_PATHandFFPROBE_PATH. An explicit path that does not exist is an error, never silently replaced. ffprobe next toFFMPEG_PATHis found automatically.PATH.~/.local/bin.- The managed copy in
.vid2context/bin/(or$VID2CONTEXT_HOME/bin). - A managed download, only when a video or stream actually needs ffmpeg, and only if
VID2CONTEXT_FFMPEG_DOWNLOADis not0. Reading metadata never triggers it.
Without ffmpeg, audio files still work (raw bytes, filesystem metadata) and sidecar subtitles still work. Video files and streams fail with a hint to install ffmpeg.
Managed download
On Linux x64 and arm64, vid2context downloads the newest release-branch static build from BtbN/FFmpeg-Builds (about 150 MB), checks it against the release's checksums.sha256, and installs ffmpeg and ffprobe into .vid2context/bin/ with a provenance file ffmpeg.json. A lock in .vid2context/locks/ makes parallel processes wait for one download instead of each starting their own.
These builds are GPL-licensed (they include x264 and x265). If that matters for how you distribute vid2context, install ffmpeg yourself and set VID2CONTEXT_FFMPEG_DOWNLOAD=0.
There is no managed download on macOS or Windows: no source with a published checksum was found (evermeet.cx, the usual macOS build, publishes GPG signatures only). Install with brew install ffmpeg or winget install ffmpeg.
Static builds and network input
Some static builds, including the johnvansickle.com builds, crash (SIGSEGV) when they open an http(s) URL, because their statically linked C library cannot resolve DNS unless nscd is running. Local files are unaffected. Streams need ffmpeg to fetch the playlist itself, so with such a build, install ffmpeg from your package manager or point FFMPEG_PATH and FFPROBE_PATH at another build (the managed BtbN build works). vid2context reports this crash with that hint.
Safety
ffmpeg runs without a shell, with arguments as an array. Inputs that start with - are refused, relative paths are made absolute (so concat: and other protocol names are never read from a path), every run has a timeout, an aborted request kills the process, and temp files are removed when done. For URL inputs ffmpeg may only use the http, https, tls, tcp and crypto protocols, so a hostile playlist cannot make it read local files.
Size limits
| Limit | Default | Change it with |
|---|---|---|
| Direct media URL download | 200 MB (1 MB = 1,048,576 bytes) | VID2CONTEXT_MAX_MEDIA_MB |
| Video URL on Workers or in the browser (no ffmpeg there) | 25 MB | not configurable: run on Node with ffmpeg instead |
| Audio file passed through without re-encoding | 25 MB | not configurable |
The URL cap is checked against content-length (from HEAD, or a one-byte ranged GET when HEAD is refused) before downloading, and again while streaming, so a server that lies about the size is still stopped. Responses that are clearly not media (an HTML page) are refused with a hint to link the file itself.
Direct video URLs
On Node, a video URL (.mp4, .webm, ...) is downloaded through the same capped fetch into a temp file, and ffmpeg extracts the audio locally. On Workers and in the browser there is no ffmpeg, so only video files up to 25 MB are accepted and sent to speech-to-text as they are.
HLS and DASH
media-stream fetches the playlist for metadata: total duration (segment durations for HLS, mediaPresentationDuration for DASH), live or on-demand, variant count, audio renditions and subtitle languages. For a multivariant HLS playlist, audio comes from the default audio rendition's own playlist when it has one, otherwise from the smallest variant (an audio-only variant when there is one), so ffmpeg fetches as little as possible. Live streams are refused; for live transcription see watch_live in providers.md.
Verified live on 2026-11-14: Apple's public bipbop example (30 minutes of audio extracted in 16 seconds) and the Tears of Steel HLS demo (CC BY 3.0) transcribed through the keyless OVH rung.
For other packages
@vid2context/ffmpeg is Node only. Never import it from core or from a provider that declares the workers or browser runtime.
| Export | Use |
|---|---|
locateFfmpeg(ctx, { download }), tryLocateFfmpeg(ctx), findTools(env) |
find the binaries and the version |
probe(input, ctx) |
duration, container, streams, tags, chapters |
extractAudio(input, { format, sampleRate, channels, bitrate, startSec, durationSec, maxBytes }, ctx) |
normalised speech audio (mp3, flac or wav) |
splitOnSilence |
the ffmpeg-silence AudioSplitter for the long-audio orchestrator: cuts near targetSec at silences, or at the quietest moment in noisy recordings, every chunk under maxBytes |
extractSubtitles(input, streamIndex?, ctx) |
a text subtitle stream as WebVTT |
extractFrames(input, { everySec, sceneThreshold, maxFrames, width }, ctx) |
JPEG frames with their timestamps |