media.nvim · View & render · vimdoc
:help media
What is in this media file, and one picture of it
doc/media.txt — rendered from the plugin's own vimdoc
*media.txt* What is in this media file, and one picture of it *media* *media.nvim*ffprobesays how long, how big and in which codec;ffmpegproduces a poster frame or a contact sheet as a PNG on disk. Anything that can draw a picture can then show a video.
CONTENTS
1. Requirements .......................... |media-requirements| 2. Setup ................................. |media-setup| 3. Commands .............................. |media-commands| 4. Keymaps ............................... |media-keymaps| 5. Configuration ......................... |media-configuration| 6. API ................................... |media-api| 7. What it does not do ................... |media-limits| 8. Health ................................ |media-health|
1. REQUIREMENTS
Neovim 0.10+ (vim.system,vim.uv) lib.nvim keymap registry, command completion ffmpeg + ffprobe one install, both binaries winget install Gyan.FFmpeg (Windows) brew install ffmpeg (macOS) sudo apt install ffmpeg (Debian / Ubuntu) After a Windows install, restart the terminal: winget and scoop extend the user PATH, and every running process inherited its copy at login. media.nvim probes both shim directories anyway, so it usually finds ffmpeg regardless. Optional: images.nvim draws rendered stills in the terminal instead of handing them to an external viewer.
2. SETUP
{
"StefanBartl/media.nvim",
dependencies = { "StefanBartl/lib.nvim" },
event = "VeryLazy",
opts = {},
}
setup()is optional for the API —require("media").frame(...)works without it — but the command and the keymaps come from it.VeryLazyrather thancmd = { "Media" }because the keymaps have to exist before you press one.
3. COMMANDS
*:Media* :Media [path] Describe the file: duration, size, bitrate, resolution, frame rate, codecs, channels. Same as:Media probe. :Media probe [path] As above. :Media frame [path] [at=…] [width=…] Render one still and show it.at=takes seconds (27.5), a percentage of the duration (50%) or a timestamp (00:01:23.5). Default:at=10%,width=800. :Media sheet [path] [rows=…] [cols=…] [width=…] Render a grid of stills spread evenly over the running time. Default: 3x4 at 1200px. :Media waveform [path] [width=…] [height=…] Render a waveform picture of the audio and show it — the sound equivalent of:Media frame. Needs an audio stream. Default: 1200x300. :Media spectrogram [path] [width=…] [height=…] Same shape as:Media waveform, a different question answered: frequency content rather than loudness. Default: 1200x300. :Media dashboard [cfile|cwd] [path=<dir>] One list across every image, PDF, audio file and video below a scope, and whether each one's text is there: image assets/error.png 1920x1080 ✓ ocr audio notes.m4a 6:44 — transcript: missing video talks/keynote.mp4 41:07 ! transcript: stale The last column is why this exists.—is a job not yet done;!is a file on disk quietly answering questions about a version of the source that no longer exists. Scope is the same three words images.nvim and language.nvim use: cwd (default), cfile, path=<dir>..gitandnode_modulesare never descended into. Keys: <CR> the obvious thing (make the text when it is missing or stale, open it when it is current), <Tab> mark a row and step down, a choose an action, o open the existing text, gf open the source, p describe it, r rescan, q close. The right mouse button offers the same actions asa. With rows marked, <CR> and a act on the marked set. A batch runs sequentially -- twelve ffmpegs at once is slower, not faster -- under ONE progress handle showing a real ratio (4/12). A failure does not stop it; whatever failed is listed by name at the end. progress_style = "float" gives the batch a cancel key. An action whose tool is missing is still listed, with the reason; picking it explains rather than runs. The scan starts no processes. The detail column is filled in afterwards, by a bounded number of probes, and the list redraws as they answer. :Media text [path] [out=] Anything to text, whatever the file is -- the kind-agnostic verb, and the reason this plugin is calledmediaand nottranscribe: image -> images.ocr.run (tesseract) pdf -> pdfport.extract audio/video -> this plugin's own dispatcher anything else -> says so, and does nothingout=depends on the kind: srt and vtt need timestamps, so they are offered for audio and video only. Each kind writes its own sidecar name -- <file>.ocr.md, <file>.text.md, <file>.transcript.md -- because OCR misreads, transcription mishears and extraction is exact, and the name is the only warning a reader gets. Every dependency is soft: a missing images.nvim costs OCR and nothing else, and says so with the fix. :Media transcribe [path] [engine=] [lang=] [task=] [out=] Speech to text: probe, extract a 16 kHz mono WAV, run it through the resolved engine, deliver it.out=is one of: buffer a scratch window (the default) sidecar <file>.transcript.md srt <file>.srt (SubRip) vtt <file>.vtt (WebVTT) An unknownout=is rejected before the run starts, not after minutes of waiting. Needswhisper-clion PATH andtranscribe.whisper_cpp.modelset; neither is ever installed automatically.:checkhealth mediasays which is missing. :Media engines List registered transcription engines and whether each reports itself available right now. :Media play [path] Hand the file to an external player (the configuredplayer, or the system default). Fire and forget. :Media window [path] [at=…] [screen=…] Play in a real mpv window: video and sound, drawn by mpv, no editor redraw in the loop.at=takes the same forms as:Media frameand is passed to mpv's--start.screen=is which display--geometry/autofitresolve against — mpv's own screen index;mpv --screen=<n>on its own finds which number is which monitor. Always mpv; the window is stopped at|:qa|. Size and behaviour are thewindowtable — see |media-configuration|. :Media cache clear Delete every rendered still. :Media health Run|:checkhealth|media. Omitting [path] uses <cfile>, then the current buffer's name.
4. KEYMAPS
Bound globally bysetup(), on the path under the cursor. <leader>Mp describe the file <leader>Mf poster frame <leader>Ms contact sheet <leader>Mo play in an external player Rebind or disable individually throughkeymaps— see |media-configuration|.
5. CONFIGURATION
require("media").setup({
bin = { ffmpeg = nil, ffprobe = nil, mpv = nil },
timeout_ms = 15000,
frame = { at = "10%", width = 800 },
sheet = { rows = 3, cols = 4, width = 1200, margin = 4,
timeout_ms = 120000 },
waveform = { width = 1200, height = 300, colors = "#9cdcfe",
timeout_ms = 120000 },
transcribe = {
engine = "whisper_cpp", fallback = {}, lang = nil,
task = "transcribe", output = "buffer", cache = true, -- buffer|sidecar|srt|vtt
timeout_ms = 0, normalize_timeout_ms = 120000,
whisper_cpp = { model = nil },
},
cache = { enabled = true, dir = nil },
render_concurrency = 4,
progress_style = "auto", -- auto|notify|statusline|fidget|float|kit
player = nil,
keymaps = {
preset = true,
probe = "<leader>Mp",
frame = "<leader>Mf",
sheet = "<leader>Ms",
play = "<leader>Mo",
which_key = true,
},
})
bin.ffmpeg, bin.ffprobe, An explicit path, for a binary installed but not
bin.mpv on PATH. Honoured even when it does not exist, so
the error names your own setting. mpv is optional
-- only |media.audio()| uses it, and its absence
just means a played run stays silent.
timeout_ms Ceiling on one ffprobe or poster-frame run.
ffmpeg reading a stalled network mount does not
fail; it waits.
frame.at 10% rather than 0 because the first frame of a
real video is usually black, a fade-in or a logo.
sheet.timeout_ms Its own, because a sheet is a pass over the whole
file where a frame is a seek. Files over two
minutes are decoded from keyframes only.
waveform.colors showwavespic's own argument; ignored by the
spectrogram. Always passed explicitly -- ffmpeg's
own default (white) draws invisibly against a
light terminal background.
waveform.timeout_ms Same reasoning as sheet.timeout_ms: a waveform
reads every sample of the file once, not a seek.
transcribe.timeout_ms 0 (no timeout) by default -- an hour of audio is
minutes of work, and the 15s interactive
timeout_ms above would kill every real run.
render_concurrency How many ffmpeg renders may run at once. A bound
on a storm, not a throughput setting: holding a
paging key down in a video hover measured 30
concurrent processes before this existed, and 60
with prefetching behind it. A playback window
jumps the queue regardless of this number -- it is
the one render with a deadline, and queued behind
thirty stills it arrived at 1111ms against the
1000ms the transport allows.
progress_style How :Media transcribe shows that it is working.
lib.nvim.progress's own style names, passed
through. "float" is the one with a cancel key:
focus it and press <Esc> in normal mode and the
whole pipeline stops, ffmpeg included. Without
lib.nvim there is no indicator and the command
says "transcribing..." once, as it always did.
transcribe.whisper_cpp.model
Absolute path to a GGML .bin file. Never set or
downloaded automatically; :checkhealth media
reports it missing rather than guessing.
cache.dir nil means stdpath("cache")/media.nvim. The key
carries the source file's mtime, so entries never
go stale and never need expiring.
player nil hands the file to the system's default
handler. A string or argv list overrides it.
:Media play only.
window.autofit, The mpv window behind |media.play_window()| and
window.ontop, :Media window. autofit is --autofit-larger
window.args (default "80%x80%"); ontop keeps it above the
terminal, on by default because a window spawned
from a terminal cannot foreground itself on
Windows; args is extra mpv flags, verbatim.
The reasoning for every default is in lua/media/config/DEFAULTS.lua.
6. API
local ok, media = pcall(require, "media")
if not ok or not media.available() then return end
media.is_video(path) -- by extension, no process started
media.is_audio(path)
media.is_media(path)
media.probed(path) -- what is known already; nil, never blocks
media.probe(path, function(probe, err) end)
media.frame(path, { at = "10%", width = 800 }, function(png, err) end)
media.prefetch_frame(path, { at = "20%", width = 800 }) -- render early,
-- no callback; for a consumer that steps
media.frames(path, { count = 24, fps = 12 }, function(pngs, err) end)
media.sheet(path, { rows = 3, cols = 4 }, function(png, err) end)
media.play(path) -- returns ok, err; hand off, not owned
media.audio(path, { at = 0 }, function(handle, err) end)
media.audio_available() -- mpv on PATH (or bin.mpv configured)?
media.play_window(path, { at = 90 }) -- returns handle, err; a real mpv window
media.transcribe(path, {}, function(transcript, err) end) -- speech to text
media.transcribe_available() -- any registered engine available?
media.player_available() -- same check, for the windowed player
media.clear_cache() -- returns the number of files removed
Every callback runs exactly once and on the main loop, so it may touch the Neovim API. A cache hit still calls back asynchronously.media.audio()startsbin.mpvaudio-only onpathover its own JSON IPC socket, and hands the callback a handle once that socket answers -- never an error when mpv is missing, justhandle == nil. The handle: handle.pause() handle.resume() handle.seek(seconds) handle.stop() handle.time_pos(function(seconds) end) Built for a consumer that draws a picture against mpv's own clock instead of a Lua timer -- see lua/media/core/audio.lua for the reasoning.media.play_window()opensbin.mpvonpathin a real window -- video and sound, drawn by mpv, no editor redraw in the loop -- and returns a handle: handle.proc the vim.system object, for the pid handle.stop() ends the window and its process tree; idempotent handle.stopped() whether stop() has been calledhandle.stop()runs for you at|:qa|even when the caller never calls it. For a consumer whose block-graphics playback is smooth (a fast terminal), this is an alternative rather than a replacement; where the editor's redraw is the ceiling it is the only thing that plays. See lua/media/core/player.lua. A probe record carries:path,container,duration,size,bitrate,has_video,has_audio,has_cover,width,height,rotation,fps,video_codec,audio_codec,channels,sample_rate. Everything butpathand the three booleans may be nil.widthandheightare the *display* pair: a 90-degree rotation is applied here, matching what ffmpeg's auto-rotation produces in the still.has_videoexcludes attached cover art, which is reported ashas_coverinstead — an mp3 with album art has a video stream, and taking that at face value makes every tagged music file look like a one-frame film.require("media.ui").summary(probe)returns the one-line form (1920x1080 · 4:32 · h264 · 100 MB).
7. WHAT IT DOES NOT DO
media.nvim does not decode video itself, and nothing *inside* a terminal Neovim can draw real moving pictures: the only image protocol that reaches the terminal from inside Neovim carries a whole picture per write and has no notion of a frame, and Neovim repaints over anything drawn between its own redraws. There are two honest answers.media.frames()+media.audio()feed a consumer that draws block graphics into a buffer (hover.nvim's video hover): text, so it survives every redraw. It is smooth on a fast terminal; where the editor's own redraw is the bottleneck it is a slideshow, and tuning the paint does not change that -- the picture is the redraw.media.play_window()/:Media windowis the other: a real mpv window, drawn by mpv with no editor redraw in the loop.:Media playis the third and least owned -- hand the file to whatever the system opens it with. On Windows a window started from a terminal Neovim opens behind the terminal: Windows grants focus only to the process owning the foreground window, and inside a terminal that is the terminal host, not nvim.exe.:Media windowpasses--ontopso it stays visible regardless.
8. HEALTH
:checkhealth media
Verifies both binaries, reports ffmpeg's version, checks that the cache
directory is writable, and reports whether mpv is found -- for media.audio()
and media.play_window(). Its absence is informational, never an error: a
played run without it is simply silent and :Media window cannot open,
exactly as before those features existed. The cache check
exists because its own failure is silent otherwise: a read-only cache
directory produces "no frame was written" from a renderer that ran
perfectly.