This is the implementation guide for video.el. Usage, build instructions, key bindings, and supported/unsupported behavior belong in the README.
video.el +-- video-view.el ----+ +-- video-inline.el --+--> video-runtime.el --> video-source.el
| Elisp module | Responsibility |
|---|---|
video-source.el | Local files and URIs, media kind, GIF metadata, HTTP headers; no native module load |
video-runtime.el | Players, sessions and leases, Canvas targets, native dispatch, shared transport |
video-view.el | Dedicated buffers, window overlays, display and file modes, navigation, gestures |
video-inline.el | Lazy occurrences, poster replacement, host-buffer lifetime |
video.el | Public load entry point and optional Evil integration |
A player owns one GStreamer decoding/clock/audio/seek session. A
video-session owns that player and its presentation leases. A dedicated
buffer or inline occurrence retains one lease; multiple windows on the same
buffer create separate render targets, not additional leases. The borrowed
player API (video-present-player or video-inline-create with :player)
does not transfer ownership. The host closes its target/presentation; the
embedding owner closes a borrowed player.
video-runtime requires video-module through Emacs’s load path. Installing
the built module alongside the Lisp files suffices; a missing module produces
the standard file-missing error. video-source can load without it.
video-target-create accepts four optional callbacks, each passed its target:
| Keyword | Contract |
|---|---|
:visible-function | Return whether this host is visible, optionally its displaying window for background lookup; absent means visible |
:prepare-function | Prepare the view for a frame, including initial fit after dimensions arrive |
:present-function | Publish the copied frame after Canvas refresh |
:close-function | Release the host attachment once on target close |
Missing prepare, present, or close callbacks do nothing. Preparation can close the target: dispatch must not copy into a target after that. Target close clears callbacks before calling host cleanup so reentrant cleanup cannot repeat itself or retain captured host objects. Native and host close errors are both reported rather than skipping either cleanup step.
Background is display state, not host policy. A target retains its display
:anchor (overlay or marker) and, for a window-specific presentation,
:window. Without an anchor it captures point. Overlays remain host-owned;
markers are copied and released on close. Standard inline insertion and
dedicated windows provide this context automatically. Custom hosts pass their
existing occurrence marker, which also identifies its buffer, rather than a
color callback. video-background-color resolves the current effective face
at that location, never at the native notification buffer’s point. Existing
visibility reconciliation also runs before redisplay: a changed background
recomposes a paused target even when its frame sequence is unchanged.
Each dispatch reconciles visibility and presents frames in one pass. A host is queried once per target in that pass; the result is not cached across redisplays, so scrolling, window changes and paused-background updates remain authoritative. A host callback can close its player before the pass finishes.
Frame arrival does not invalidate every buffer’s mode line. The runtime reports metadata changes and whole-second video position changes to dedicated views, which refresh only their own buffers. Inline GIF frames do not trigger global mode-line refreshes.
The dedicated host owns window parameters, overlays, per-window viewport
state, and pan timers. The inline host owns poster replacement and
occurrence cleanup. Both use the same runtime visibility policy: only windows
in visible, non-iconified frames count as visible. When all targets are hidden,
video-pause-when-hidden suspends playback without changing the desired state.
The player remembers its first target attachment; removing all targets keeps
it hidden rather than reverting to the never-presented headless-player policy.
Host cleanup may release a session lease; the final lease closes an auto-closing
session.
Player creation starts asynchronous paused preroll. A dedicated still-image viewer therefore receives dimensions and a first frame without requesting playback; desired state remains paused. Setting the URI alone is not preroll.
All six C translation units link into video-module.so:
| Component | Responsibility |
|---|---|
src/video-module.c | Emacs values and errors, user pointers/finalizers, Canvas borrowing, function registration |
src/video-runtime.c | GStreamer and media state, private session/target handles, workers, frame conversion/publication |
src/video-canvas.c | BGRA composition, transport layout, and drawing |
src/video-animation.c | Shared appsrc transport, ownership, cancellation, seek/EOS and poster samples |
src/video-lottie.c | dotLottie rasterization, immutable pooled frames and retained render targets |
src/video-apng.c | FFmpeg APNG demux/decoder, retained raw frames, variable timing and dependency-aware seek |
src/video-runtime.h exposes opaque session and target handles;
src/video-animation.h defines the internal backend contract.
Only the module bridge calls Emacs APIs. Runtime control
and query operations belong to the caller’s thread; callbacks and rendering
use runtime synchronization. Native construction consumes its notification
descriptor and request-header strings on success and failure. Explicit
close consumes the caller’s handle reference synchronously; a GC finalizer
hands it to the reaper. Session/target references, worker shutdown, and
frame generation checks stay in the runtime.
GstPlay API messages transfer their bus-owned reference into the session’s
GstAtomicQueue before writing the notification byte; the sync handler returns
GST_BUS_DROP. Only the caller’s poll drains this FIFO and updates transport
state. Waking before publication would lose the final pause, seek, EOS, or error
update. Close drains both this FIFO and the ordinary bus after removing the
sync handler, keeping message-held GstPlay references off its worker’s disposal
path.
APNG and Lottie replace only GstPlay’s source via source-setup and appsrc://.
VideoAnimation owns their shared references, lock, appsrc callbacks,
cancellation, error publication and one-pass EOS. The two concrete backends
supply frames, seeking and resource cleanup; there is no decoder registry.
Playback construction stays lazy in source-setup, while standalone metadata
and poster queries are synchronous.
The Lottie renderer writes directly into system-memory GstBufferPool allocations; published frames and retained targets are never overwritten. The renderer’s bound allocation remains alive until rebinding or destruction. Seek moves the source frame index; closing flushes blocked pool acquisition before joining playback. Source identity restores the premultiplied-alpha flag after caps negotiation, including the subtitle conversion path.
APNG scans packets with bounded memory to obtain duration and check stream completeness; playback borrows refcounted AVFrame pixels with explicit row strides, converting only unsupported raw formats. Published samples retain their pixels independently of the decoder. FFmpeg handles APNG blend/disposal; seeking recreates decoder state and decodes dependencies without publishing pre-target frames. Appsrc emits one pass with original timestamps; the shared player owns loop policy.
GStreamer callbacks do not call Emacs. The appsink retains the newest sample
and wakes the render worker. Alpha input is normalized into a reusable,
premultiplied native-endian ARGB32 staging frame shared by the targets; RGBx
padding is normalized to opaque alpha. Premultiplication happens before
resampling so hidden RGB cannot leak into scaled edges. For each target,
GstVideoConverter converts the source crop intersecting the absolute
(scale, x, y) viewport into viewport-sized front/back buffers with transparent
padding, not a scaled copy of the entire source. Frames from older viewport
generations are rejected, including frames superseded by a pan or zoom at
unchanged dimensions. If rendering falls behind, frames are dropped while
GStreamer’s audio clock remains authoritative.
A swapped buffer sends one byte through the notification descriptor opened
with emacs_env::open_channel. Its Emacs pipe-process filter schedules
main-thread dispatch. The native copy call borrows canvas_data and flattens
the clipped region against that target’s current background in one copy pass,
producing opaque pixels even on RGB24 Canvas backends. It does not blend
against previous Canvas pixels, so transparent frames erase old content.
Elisp draws transport controls, calls canvas-refresh, and invokes the host’s
presentation callback. No Canvas pointer survives a native call or crosses
resize/redisplay. Source and target frames remain independent of host colors.
A target may copy its frame into a destination rectangle of a larger, application-owned Canvas. The application owns the static scene, item hit maps, and navigation offsets; video.el owns decoding, target pixels, transport controls, and their hot spots, preserving host hit maps. A Canvas cannot be nested inside SVG: an SVG carousel must move its entire scene to Canvas before playing video in place. The host can then compose static items and dynamic regions and refresh the scene once.
video-canvas-create accepts an optional Emacs color string, such as
"#ff8000", "#f80", or "orange", for the background;
video-canvas-draw-source accepts it as :background alongside :fit.
Playback and poster APIs derive the native decoder selector from
video-source.el’s local-content recognition; callers do not override it.
Both default to video-background-color at the calling buffer position. Scene
hosts resolve their static drawing background at the same location supplied
as the inline :anchor. Hosts remain responsible for redrawing their
one-shot static scene when its colors change; target composition is automatic.
Emacs displays a Canvas as an IMAGE_GLYPH in a text glyph row. Graphical
redisplay reserves one canonical character column at the end of the row for
cursor and truncation/continuation machinery. append_space_for_newline
requires an end-of-line glyph where a GUI cursor could be drawn, and
produce_image_glyph crops a window-width image to keep that glyph on the
same row. Hiding the cursor does not bypass the reservation.
A target sized to window-body-width can therefore lose approximately
frame-char-width pixels on the right. Fringe settings or face remapping
cannot recover those pixels. The frame-wide no-special-glyphs parameter
removes the reservation but also changes unrelated windows’ redisplay;
video-open and video-open-other-window do not set it. The independent
video-open-other-frame presentation uses it only in its own frame. Fixing
this in an ordinary window without frame-wide side effects needs window-local
redisplay support from Emacs.