Video

A <video> in every language, decoded by the operating system at the size it is drawn, frames in one GPU texture updated in place, and nothing decoded while it is off screen. Backends, API, controls, performance and limits.

BUI plays video with the same machinery as animated images: one codec interface for GIF, WebP and video (Blender’s MovieReader shape: open, next frame, seek, close), one clock (play.c) and the same worker threads. What is different is where the frames come from and where they go.

  • The operating system decodes. Windows uses Media Foundation (DXVA hardware decoding through its own Direct3D 11 device, scaling on the GPU), Linux uses GStreamer (VA-API through its va plugin where the driver has it, software otherwise), macOS uses AVFoundation / VideoToolbox. They are loaded at run time: a machine without them (Windows “N” editions, a Linux box without GStreamer) starts normally and just reports no video. Nothing is linked: on Windows the video feature adds 3 KB to a fat-LTO binary (Binary size).
  • Decoded at the size drawn. A 4K file in a 640 px box is decoded and scaled by the decoder (on the GPU on Windows) to 640 px, as NV12 (1.5 bytes a pixel). Resizing within 0.5x to 1.25x of that size just scales when drawing; beyond it the decoder switches size where it was.
  • One texture, updated in place. Two NV12 frames per player are allocated once (the one shown and the next, decoded ahead), then reused: no allocation per frame. On the GPU renderers each player has one luma and one chroma texture updated in place; the shader converts BT.601 / BT.709 YCbCr to RGB. The software raster converts only the pixels it draws, while drawing.
  • Only what is on screen plays. A video scrolled away, clipped, in a hidden window, or in a window that isn’t rendering pauses: no decoding, no timers, no redraws. It continues from where it was when it shows again.

Showing a video

use blenderui::prelude::*;
// the element owns its player: muted autoplay (autoplay="false" to hold the
// first frame), loop, poster
rsx! { <video src="intro.mp4" loop poster="intro.jpg" class="w-[640px] h-[360px] object-cover" /> }
// or keep the player: play, pause, seek, rate, time, duration, state events
let clip = std::rc::Rc::new({
let mut v = blenderui::Video::new("intro.mp4").unwrap();
v.on_state(|s| if s == blenderui::VideoState::Ended { println!("done") });
v
});
video().video(clip.clone()).w(px(640.0)).h(px(360.0));
clip.seek(std::time::Duration::from_secs(10));
clip.set_rate(2.0);
// in a canvas
canvas(move |b, p| p.video(b, &clip, blenderui::sys::BK_FIT_CONTAIN));

Defaults. A video autoplays muted (what browsers allow without a click), plays once (loop to repeat), and fits its box like object-fit: contain. Its size is 300 x 150 (HTML’s) until the first frame tells the real one. poster shows an image until then.

Controls. controls on a "video" node (C, Python’s Video(controls=True)) draws Blender’s widgets over the bottom of the video: a play/pause button and a number-slider scrubber showing 0:12 / 1:30, like the Timeline’s playback controls and frame slider. Click to play or pause, drag the scrubber to seek (the video holds while you drag), Space toggles. Rust and JavaScript elements don’t draw controls yet: build them from the player’s methods.

Events. One listener per player (BK_video_set_listener, Rust on_state, Python on_state / on_ended) hears every state change: loading, playing, paused, ended, error. JavaScript elements have no media events yet.

Backends

Backend Where Default Notes
Media Foundation Windows on (video) IMFSourceReader with a DXVA IMFDXGIDeviceManager (one Direct3D 11 video device for every player) and the reader’s video processor: hardware decoding and scaling. H.264, HEVC*, VP9*, AV1* (*with the Store extensions). Tested on Windows 11.
GStreamer Linux on (video) filesrc ! decodebin ! videoconvert ! videoscale ! appsink, dlopened. Hardware decoding where GStreamer’s va plugin finds VA-API, else software (libav, openh264, dav1d plugins). Chosen over raw VA-API: it brings the demuxers and every system decoder; VA-API alone would need our own demuxers and a software fallback anyway. Compiled and syntax-checked under WSL; see Platforms.
AVFoundation macOS on (video) AVAssetReader with VideoToolbox scaling, weak-linked frameworks. Untested: written against the documented API without a Mac; it compiles only on Apple platforms, so it can’t affect others.
dav1d, FFmpeg any not written Planned as optional features (off by default): dav1d (BSD-2) for AV1 where the OS lacks it, with a small MP4/WebM demuxer; FFmpeg for everything (LGPL or GPL by its build, patent-encumbered codecs: opt-in only). They are not in the feature table until they exist.

Formats are the OS’s: what Windows’ Movies & TV, GStreamer’s gst-play-1.0 or QuickTime Player plays, BUI plays. BK_video_backends() / bui.video_backends / Video::backends() name the ones a build has. Audio isn’t played yet (every video is muted); see Limits.

How it works

  1. BK_video_new makes a private entry of the image cache: the same requests, worker threads and visibility tracking as an <img>, but owned by the player (never shared or evicted) and with its own clock.
  2. The first draw asks for the first frame at the drawn size. A worker opens the file with the first backend that reads it (sniffing the container: MP4/MOV, WebM/MKV, AVI, ASF, Ogg, MPEG-PS/TS, FLV), plans the decode size from the intrinsic size and the box (imb_plan, as for images) and decodes NV12 into a fresh frame.
  3. While it shows, the clock asks for the next frame one step ahead; it decodes into the frame the screen let go of. When its time comes (the frame’s duration over the rate), it is swapped in and only its rectangle redraws, on the window’s frame timer.
  4. A seek drops the frame decoded ahead, seeks the decoder to the keyframe before the time and decodes forward to the frame showing then; it shows as soon as it is ready, paused or not.
  5. On the GPU, the frame’s planes are uploaded into the player’s two textures (UpdateSubresource / glTexSubImage2D) and one instanced quad samples them; a parity test checks the CPU and GPU conversions agree to within 2 levels.

Performance

Video: render time per frame

H.264 at 30 fps, Media Foundation (DXVA), decoded at the size drawn

Lower is better

0decodes, renders and allocations per frame while off screen

  • p50
  • p99
  • 1080p x 1CPU renderer
    • p50: 18.7 ms
    • p99: 24.6 ms
  • 1080p x 1GPU renderer
    • p50: 0.99 ms
    • p99: 48 ms
  • 1080p x 5CPU renderer
    • p50: 5.2 ms
    • p99: 17 ms
  • 1080p x 5GPU renderer
    • p50: 0.54 ms
    • p99: 4.9 ms
  • 4K x 1CPU renderer
    • p50: 19.7 ms
    • p99: 33.6 ms
  • 4K x 1GPU renderer
    • p50: 1.04 ms
    • p99: 51 ms
Open bench
Video: H.264 clips (testsrc2, 30 fps, 12 s, bench/video/make_clips.sh) looping, muted, each in its own box (object-fit: contain) of a headless window, driven like a window loop for 8 s, then off screen for 3 s
Clip x players (box)RendererFirst frameRender p50 / p99CPU (one core)Frames shown per sDroppedWorking setMEM allocs per frameOff screen, 3 s
1080p x 1 (1920 x 1080)CPU372 ms18.7 / 24.6 ms26 %29.90.5 %132 MB00 decodes, 0 renders, 0 ms
GPU257 ms0.99 / 48.0 ms9 %30.00 %274 MB00 decodes, 0 renders, 0 ms
1080p x 5 (640 x 360 each)CPU515 ms5.2 / 17.0 ms70 %150.10 %530 MB00 decodes, 0 renders, 0 ms
GPU560 ms0.54 / 4.9 ms28 %149.90.1 %448 MB00 decodes, 0 renders, 0 ms
4K x 1 (1920 x 1080)CPU494 ms19.7 / 33.6 ms24 %30.00 %310 MB00 decodes, 0 renders, 31 ms
GPU676 ms1.04 / 51.0 ms10 %30.00 %447 MB00 decodes, 0 renders, 0 ms

Ryzen 7 5800U (Radeon integrated GPU: its video memory is system memory, so decoder surfaces count in the working set), Windows 11, Media Foundation with DXVA. First frame: from BK_video_new to every player showing a frame (opening the files, the decoders and, the first time, the Direct3D 11 video device). CPU: process CPU time (UI thread, decode workers and Media Foundation's threads) over wall time, of one of 16 logical cores; repeated runs varied by up to 1.6x (5 players, CPU renderer: 44 % and 70 %). Dropped: frames the clips' 30 fps wanted in the measured time that never showed. The GPU p99 is one slow frame among ~240 (a driver-side upload stall); p50 is the typical frame. MEM allocations per rendered frame: frames, requests and textures are reused. Off screen: no decode, timer or render; one 4K run spent 31 ms of CPU time in the 3 s (a step in flight). Every MEM block is freed after closing. Program: bench/video/video_bench.c (bui_video_bench <clip> <count> <w> <h> [gpu] [seconds]).

Source: bench/video/video_bench.c · Ryzen 7 5800U laptop, Windows 11, on AC power

Video: CPU time

Process CPU (all threads) over wall time, of one core, while playing

Lower is better

  • 1080p x 1GPU renderer9 %
  • 4K x 1GPU renderer10 %
  • 1080p x 5GPU renderer28 %
  • 1080p x 1CPU renderer26 %
  • 4K x 1CPU renderer24 %
  • 1080p x 5CPU renderer70 %
Open bench
Video: H.264 clips (testsrc2, 30 fps, 12 s, bench/video/make_clips.sh) looping, muted, each in its own box (object-fit: contain) of a headless window, driven like a window loop for 8 s, then off screen for 3 s
Clip x players (box)RendererFirst frameRender p50 / p99CPU (one core)Frames shown per sDroppedWorking setMEM allocs per frameOff screen, 3 s
1080p x 1 (1920 x 1080)CPU372 ms18.7 / 24.6 ms26 %29.90.5 %132 MB00 decodes, 0 renders, 0 ms
GPU257 ms0.99 / 48.0 ms9 %30.00 %274 MB00 decodes, 0 renders, 0 ms
1080p x 5 (640 x 360 each)CPU515 ms5.2 / 17.0 ms70 %150.10 %530 MB00 decodes, 0 renders, 0 ms
GPU560 ms0.54 / 4.9 ms28 %149.90.1 %448 MB00 decodes, 0 renders, 0 ms
4K x 1 (1920 x 1080)CPU494 ms19.7 / 33.6 ms24 %30.00 %310 MB00 decodes, 0 renders, 31 ms
GPU676 ms1.04 / 51.0 ms10 %30.00 %447 MB00 decodes, 0 renders, 0 ms

Ryzen 7 5800U (Radeon integrated GPU: its video memory is system memory, so decoder surfaces count in the working set), Windows 11, Media Foundation with DXVA. First frame: from BK_video_new to every player showing a frame (opening the files, the decoders and, the first time, the Direct3D 11 video device). CPU: process CPU time (UI thread, decode workers and Media Foundation's threads) over wall time, of one of 16 logical cores; repeated runs varied by up to 1.6x (5 players, CPU renderer: 44 % and 70 %). Dropped: frames the clips' 30 fps wanted in the measured time that never showed. The GPU p99 is one slow frame among ~240 (a driver-side upload stall); p50 is the typical frame. MEM allocations per rendered frame: frames, requests and textures are reused. Off screen: no decode, timer or render; one 4K run spent 31 ms of CPU time in the 3 s (a step in flight). Every MEM block is freed after closing. Program: bench/video/video_bench.c (bui_video_bench <clip> <count> <w> <h> [gpu] [seconds]).

Source: bench/video/video_bench.c · Ryzen 7 5800U laptop, Windows 11, on AC power

Limits

  • No audio yet: every video is muted. A small mixer (miniaudio) behind an optional feature is the plan.
  • Not zero-copy on Windows: frames are decoded and scaled on the GPU, then copied back (at the drawn size) and uploaded to the renderer’s own device. Sharing the decoder’s textures with the renderer’s device is the next step.
  • 8-bit output: 10-bit (P010) and HDR video are converted to 8-bit NV12 by the decoder.
  • No dav1d or FFmpeg backend yet: a format the OS can’t decode doesn’t play (state: error).
  • Rust and JavaScript <video> elements have no built-in controls and JavaScript has no media events.
  • An <img src="clip.mp4"> doesn’t play video (as in browsers): use <video>.
Edit this page on GitHub

Last updated