In-World Video Playback (FFmpeg)
This document describes the current FFmpeg video playback integration end-to-end: implementation architecture, Lua API and widget usage, telemetry, testing, operations, and targeted next improvements.
Goals
- Decode video frames and expose them as regular engine textures.
- Support playback in arbitrary widgets that consume textures.
- Keep A/V clocking stable with explicit observability.
- Fail loudly when required dependencies/features are unavailable.
Build and Runtime Requirements
Enable FFmpeg integration with SPACE_ENABLE_FFMPEG=ON (default in this repo), and install:
ffmpeglibavcodec-devlibavformat-devlibavutil-devlibswscale-devlibswresample-dev
When unavailable at build time, the Lua video module exposes:
video.available = falsevideo.missing-reason = "..."
Attempting to construct VideoPlayer fails with a clear runtime error.
Architecture
C++ Core
src/video_player.hsrc/video_player.cppsrc/video_telemetry.h
VideoPlayer owns:
- Decode thread (
decode_loop) for FFmpeg demux/decode. - Bounded queues for decoded video frames and audio chunks.
- GL texture upload path (
upload_frame). - Playback state (
play/pause/seek/stop,position, loop/end handling). - Audio streaming source lifecycle via engine
Audio. - Thread-safe telemetry for status inspection.
VideoManager owns:
- Registered
VideoPlayerinstances. - Engine-wide per-frame
update_alland lifecycle coordination. - Audio backend reset fanout to players.
Lua Bindings
src/lua_video.hsrc/lua_video.cpp
Binding model:
require :videoreturnsVideoPlayerfactory + availability flags.VideoPlayeris engine-owned and constructed via options table.status()returns structured playback + telemetry fields.
Widget Integration
assets/lua/video-widget.fnl
The integration path is texture-first:
- Create a
VideoPlayer. - Use
player:texture()anywhere a texture is accepted (Image,RawImage, etc.). - Let
VideoManagerdrive updates each frame globally.
This avoids coupling playback lifecycle to individual widget update hooks and allows arbitrary widget placement/composition.
Lua API
Constructor
(local Video (require :video))
(local player
(Video.VideoPlayer {:path "lua/tests/data/test-videos/01_baseline_h264_with_audio.mp4"
:loop true
:autoplay true
:muted false
:positional-audio false}))Options:
:path(required):loop(optional, defaultfalse):autoplay(optional, defaulttrue):muted(optional, defaultfalse):positional-audio(optional, defaultfalse):audio-position(optionalglm.vec3, default(0 0 0)):audio-velocity(optionalglm.vec3, default(0 0 0)):audio-direction(optionalglm.vec3, default(0 0 0)):audio-gain(optional, default1.0):audio-pitch(optional, default1.0):audio-max-distance(optional, default300.0):audio-rolloff-factor(optional, default0.05):audio-reference-distance(optional, default10.0):audio-min-gain(optional, default0.0):audio-max-gain(optional, default1.0):audio-cone-inner-angle(optional, default360.0):audio-cone-outer-angle(optional, default360.0):audio-cone-outer-gain(optional, default0.0)
Methods
play()pause()stop()seek(seconds)update(dt-ms)(normally manager-driven)drop()ready()ended()playing()duration()position()texture()status()set-positional-audio(enabled)positional-audio()set-audio-position(position-vec3)
Status Telemetry Fields
Playback state:
readyendedplayinghas-errorerror
Clocking:
clock-secondshas-audio-clockav-drift-secondsmax-av-drift-secondsrecent-max-av-drift-secondsav-drift-window-seconds
Queue/backpressure:
queued-audio-chunksdropped-audio-chunks(queue-overflow drops)flushed-audio-chunks(explicit seek/stop flushes)dropped-video-frames
Decode-thread telemetry:
decode-loop-iterationsdecode-wait-ms
Audio presence:
audio-availableaudio-activepositional-audio
Telemetry Semantics
dropped-audio-chunksmeans chunks evicted due to bounded queue pressure.flushed-audio-chunksmeans chunks intentionally discarded on control operations (seek,stop).recent-max-av-drift-secondsis the max absolute drift seen within a rolling window (av-drift-window-seconds, currently 2.0).decode-wait-msis cumulative decode-thread wait time while buffers are full (useful backpressure signal, monotonic counter).decode-loop-iterationsis cumulative decode-loop iterations (liveness/throughput signal, monotonic counter).
Tests
Lua/Fennel
assets/lua/tests/test-video.fnlassets/lua/tests/test-video-widget.fnlassets/lua/tests/test-video-soak.fnl
Coverage includes:
- Decode-to-texture readiness.
- Seek/end/loop behavior.
- Pause/resume stability.
- Multi-player concurrency.
- Structured status field contracts.
- Audio flush telemetry on seek.
- Decode telemetry monotonicity under buffered and consuming phases.
- Audio drift budget checks (when audio clock available).
Native C++
tests/test_video_telemetry.cpp
Deterministic checks:
- Drift window and absolute max math (
VideoAvDriftTracker). - Dropped/flushed chunk counter correctness (
VideoAudioChunkCounters).
Soak Validation
Run soak:
SPACE_ASSETS_PATH=$(pwd)/assets ./build/space -m tests.test-video-soak:mainDefaults:
- Duration:
600s - Poll sleep:
0.01s - Max recent drift budget:
0.35s - Decode wait budget: disabled unless configured
Override knobs:
SPACE_VIDEO_SOAK_SECONDSSPACE_VIDEO_SOAK_SLEEP_SECONDSSPACE_VIDEO_SOAK_MAX_RECENT_DRIFT_SECONDSSPACE_VIDEO_SOAK_MAX_DECODE_WAIT_MS
Example:
SPACE_ASSETS_PATH=$(pwd)/assets \
SPACE_VIDEO_SOAK_SECONDS=120 \
SPACE_VIDEO_SOAK_SLEEP_SECONDS=0.01 \
SPACE_VIDEO_SOAK_MAX_RECENT_DRIFT_SECONDS=0.25 \
SPACE_VIDEO_SOAK_MAX_DECODE_WAIT_MS=5000 \
./build/space -m tests.test-video-soak:mainNote: SPACE_DISABLE_AUDIO=1 is still useful for deterministic CI/sandbox runs, but audio drift realism requires audio enabled.
Known Constraints
- Decode and queue metrics are cumulative; interpret them as trend counters, not instantaneous gauges.
decode-wait-mscan increase during healthy operation when decoder is ahead and waiting on bounded queues.- Audio-clock strictness in tests is gated to avoid false failures when audio is disabled/unavailable.
Potential Improvements
- Add instantaneous/rate telemetry (per-second deltas for queue pressure and decode wait).
- Add explicit decode stall detector (
no iterations advanced over N ms while playing). - Add live in-world telemetry panel for active players.
- Add targeted audio-device reset/replug scripted validation.
- Add adaptive queue sizing based on stream properties and measured jitter.
