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*

ffprobe says how long, how big and in which codec; ffmpeg produces a
poster frame or a contact sheet as a PNG on disk. Anything that can draw a
picture can then show a video.

CONTENTS *media-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 *media-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 *media-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.

VeryLazy rather than cmd = { "Media" } because the keymaps have to exist
before you press one.

3. COMMANDS *media-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>.
                        .git and node_modules are 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 as a.

                        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
                        called media and not transcribe:
                          image       -> images.ocr.run (tesseract)
                          pdf         -> pdfport.extract
                          audio/video -> this plugin's own dispatcher
                          anything else -> says so, and does nothing
                        out= 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 unknown out= is rejected before the run
                        starts, not after minutes of waiting.
                        Needs whisper-cli on PATH and
                        transcribe.whisper_cpp.model set; neither is ever
                        installed automatically. :checkhealth media says
                        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 configured
                        player, 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 frame and is passed to mpv's
                        --start. screen= is which display --geometry/
                        autofit resolve 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 the window table —
                        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 *media-keymaps*

Bound globally by setup(), 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 through keymaps — see
|media-configuration|.

5. CONFIGURATION *media-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 *media-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() starts bin.mpv audio-only on path over its own JSON IPC
socket, and hands the callback a handle once that socket answers -- never an
error when mpv is missing, just handle == 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() opens bin.mpv on path in 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 called

handle.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 but
path and the three booleans may be nil.

width and height are the *display* pair: a 90-degree rotation is applied
here, matching what ffmpeg's auto-rotation produces in the still.

has_video excludes attached cover art, which is reported as has_cover
instead — 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-limits*

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 window is the other: a real mpv window, drawn
by mpv with no editor redraw in the loop. :Media play is 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 window
passes --ontop so it stays visible regardless.

8. HEALTH *media-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.