Skip to content

Latest commit

 

History

History
197 lines (166 loc) · 11.4 KB

File metadata and controls

197 lines (166 loc) · 11.4 KB

video.el architecture

This is the implementation guide for video.el. Usage, build instructions, key bindings, and supported/unsupported behavior belong in the README.

Ownership and module boundaries

video.el
  +-- video-view.el ----+
  +-- video-inline.el --+--> video-runtime.el --> video-source.el
Elisp moduleResponsibility
video-source.elLocal files and URIs, media kind, GIF metadata, HTTP headers; no native module load
video-runtime.elPlayers, sessions and leases, Canvas targets, native dispatch, shared transport
video-view.elDedicated buffers, window overlays, display and file modes, navigation, gestures
video-inline.elLazy occurrences, poster replacement, host-buffer lifetime
video.elPublic 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.

Target and host contract

video-target-create accepts four optional callbacks, each passed its target:

KeywordContract
:visible-functionReturn whether this host is visible, optionally its displaying window for background lookup; absent means visible
:prepare-functionPrepare the view for a frame, including initial fit after dimensions arrive
:present-functionPublish the copied frame after Canvas refresh
:close-functionRelease 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.

Native module and rendering

All six C translation units link into video-module.so:

ComponentResponsibility
src/video-module.cEmacs values and errors, user pointers/finalizers, Canvas borrowing, function registration
src/video-runtime.cGStreamer and media state, private session/target handles, workers, frame conversion/publication
src/video-canvas.cBGRA composition, transport layout, and drawing
src/video-animation.cShared appsrc transport, ownership, cancellation, seek/EOS and poster samples
src/video-lottie.cdotLottie rasterization, immutable pooled frames and retained render targets
src/video-apng.cFFmpeg 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.

Composite Canvas hosts

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.

Why ordinary windows can clip the right edge

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.