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
vaplugin 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 thevideofeature 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, posterrsx! { <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 eventslet 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 canvascanvas(move |b, p| p.video(b, &clip, blenderui::sys::BK_FIT_CONTAIN));// React and @blender-ui/native's DOM API (and `bui build` apps): the same <video> as Rust<video src="intro.mp4" loop poster="intro.jpg" style={{ width: 640, height: 360, objectFit: 'cover' }} /><video src="intro.mp4" autoplay="false" /> {/* holds the first frame */}import blenderui as buifrom blenderui.components import Video
v = Video("intro.mp4", controls=True, loop=True, poster="intro.jpg", fit="cover", on_ended=lambda: print("done"))v.pause(); v.seek(10.0); v.play()v.player.rate = 2.0 # the bui.Video behind itprint(v.player.time, v.player.duration, v.player.state == bui.VIDEO_PLAYING)bui.video_backends # "mf", "gstreamer", "avfoundation"; "" without videoBKVideo *v = BK_video_new("intro.mp4"); /* muted autoplay; loads when first drawn */BK_video_set_loop(v, 1);BK_video_set_controls(v, 1); /* Blender-style controls on "video" nodes */BK_video_set_listener(v, on_state, app); /* loaded, playing, paused, ended, error */
BKNode *n = BK_node_new("video"); /* the frame, fitted, and its controls */BK_node_set_video(n, v, BK_FIT_CONTAIN);
BKCanvasItem it = {.type = BK_ITEM_IMAGE, .coords = (float[]){0, 0, 640, 360}, .ncoords = 4, .video = v, .fit = BK_FIT_COVER}; /* or on a canvas */BK_canvas_set_item(canvas, &it);
BK_video_seek(v, 10.0);BK_video_set_rate(v, 2.0);double t = BK_video_time(v), d = BK_video_duration(v);BK_video_free(v); /* after the node / item let go */import blenderui as buifrom blenderui import imgui
clip = bui.Video("intro.mp4")tex = 0# every frame:tex = clip.texture(tex, 640, 360) # the current frame, updated in place (RGBA)if tex: imgui.image(tex, clip.size or (640, 360))C: BK_video_texture(v, tex, max_w, max_h); Rust: Video::texture. Calling it each frame is what keeps the video playing; keep rendering while BK_video_state is BK_VIDEO_PLAYING.
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
BK_video_newmakes 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.- 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. - 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.
- 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.
- 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 benchClose bench
| Clip x players (box) | Renderer | First frame | Render p50 / p99 | CPU (one core) | Frames shown per s | Dropped | Working set | MEM allocs per frame | Off screen, 3 s |
|---|---|---|---|---|---|---|---|---|---|
| 1080p x 1 (1920 x 1080) | CPU | 372 ms | 18.7 / 24.6 ms | 26 % | 29.9 | 0.5 % | 132 MB | 0 | 0 decodes, 0 renders, 0 ms |
| GPU | 257 ms | 0.99 / 48.0 ms | 9 % | 30.0 | 0 % | 274 MB | 0 | 0 decodes, 0 renders, 0 ms | |
| 1080p x 5 (640 x 360 each) | CPU | 515 ms | 5.2 / 17.0 ms | 70 % | 150.1 | 0 % | 530 MB | 0 | 0 decodes, 0 renders, 0 ms |
| GPU | 560 ms | 0.54 / 4.9 ms | 28 % | 149.9 | 0.1 % | 448 MB | 0 | 0 decodes, 0 renders, 0 ms | |
| 4K x 1 (1920 x 1080) | CPU | 494 ms | 19.7 / 33.6 ms | 24 % | 30.0 | 0 % | 310 MB | 0 | 0 decodes, 0 renders, 31 ms |
| GPU | 676 ms | 1.04 / 51.0 ms | 10 % | 30.0 | 0 % | 447 MB | 0 | 0 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 benchClose bench
| Clip x players (box) | Renderer | First frame | Render p50 / p99 | CPU (one core) | Frames shown per s | Dropped | Working set | MEM allocs per frame | Off screen, 3 s |
|---|---|---|---|---|---|---|---|---|---|
| 1080p x 1 (1920 x 1080) | CPU | 372 ms | 18.7 / 24.6 ms | 26 % | 29.9 | 0.5 % | 132 MB | 0 | 0 decodes, 0 renders, 0 ms |
| GPU | 257 ms | 0.99 / 48.0 ms | 9 % | 30.0 | 0 % | 274 MB | 0 | 0 decodes, 0 renders, 0 ms | |
| 1080p x 5 (640 x 360 each) | CPU | 515 ms | 5.2 / 17.0 ms | 70 % | 150.1 | 0 % | 530 MB | 0 | 0 decodes, 0 renders, 0 ms |
| GPU | 560 ms | 0.54 / 4.9 ms | 28 % | 149.9 | 0.1 % | 448 MB | 0 | 0 decodes, 0 renders, 0 ms | |
| 4K x 1 (1920 x 1080) | CPU | 494 ms | 19.7 / 33.6 ms | 24 % | 30.0 | 0 % | 310 MB | 0 | 0 decodes, 0 renders, 31 ms |
| GPU | 676 ms | 1.04 / 51.0 ms | 10 % | 30.0 | 0 % | 447 MB | 0 | 0 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>.
Last updated