Video
VideoRenderable plays an MP4 file with H.264 video and optional AAC audio. Decoding, the media clock, audio, and frame drawing run in native code. Decoded frames never cross into JavaScript.
Availability#
| Field | Availability |
|---|---|
| Package | @opentui/core |
| Core renderable | VideoRenderable |
| React | <video> (automatic) |
| Solid | <video> (automatic) |
| Status | Built in |
Renderable API#
import { VideoRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const video = new VideoRenderable(renderer, {
id: "player",
source: "./clip.mp4",
width: 60,
height: 20,
autoplay: true,
loop: true,
onError: console.error,
})
renderer.root.add(video)source is a file path. Opening is synchronous: ready and videoMetadata are set when the setter returns. onReady, onError, and the onPlay of an autoplay run in a microtask, so callbacks assigned after source still receive the result. Playback that the open starts (autoplay, or a replaced source that was playing) starts in that microtask too, so options such as audio set after source apply first; playing is true from the start. Setting autoplay after source starts the open source if it has not started yet. Replacing source keeps playback running if it was. Setting it to undefined closes the player. Changing audio reopens an open source the same way, from the start.
crop shows one region of the source, in source pixels. A video wall or puzzle can give each tile its own region of one clip:
// The top-left quarter of a 1920x1080 clip.
const tile = new VideoRenderable(renderer, {
source: "./clip.mp4",
crop: { left: 0, top: 0, width: 960, height: 540 },
width: 30,
height: 10,
})React#
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
const renderer = await createCliRenderer()
createRoot(renderer).render(<video source="./clip.mp4" autoplay loop style={{ width: 60, height: 20 }} />)Solid#
import { createCliRenderer } from "@opentui/core"
import { render } from "@opentui/solid"
const renderer = await createCliRenderer()
await render(() => <video source="./clip.mp4" autoplay loop style={{ width: 60, height: 20 }} />, renderer)Playback#
- While a video plays, it ticks on the renderer’s animation clock at
maxFps. Each tick renders a frame and reads playback state, drawn or not, so a hidden or scrolled-out video keeps time, ends, and loops. A native audio thread refills audio between ticks. Each paint draws the current frame. - With an audio track, audio output is the media clock. Without audio, or if the audio device fails, playback follows the wall clock.
- Each player with audio opens its own audio device, even when
muted. For many silent players of one clip, such as tiles of a video wall, setaudio: falseon all but the one that plays sound: they open no audio device and follow the wall clock. - Frames present at the source frame rate or
maxFps, whichever is lower. seek()clamps to[0, duration]. Seeking past the end does not wrap, even whenloopis on.- Frames draw like an Image with
protocol: "auto".fitandcropwork the same way. - With
crop, the decoder scales and copies only that region of each frame, and Kitty output sends only the region. Each video still decodes the whole stream. - With Kitty output, the decoder lowers its decode size while frames take longer than the frame interval or images wait because their data does not fit one frame, and raises it again after they recover.
qualityTierreports the step;0is full size. - Set
OPENTUI_EXPERIMENTAL_VIDEO_HWACCEL=1to try VideoToolbox decoding on macOS. See Environment variables.
Options and state#
| Member | Type | Description |
|---|---|---|
source |
string | undefined |
MP4 file path. An empty string closes the player |
fit |
"fit" | "cover" | "fill" |
Destination sizing. Defaults to "fit" |
crop |
ExtractOptions | undefined |
Source region { left, top, width, height } in pixels. fit applies to it |
autoplay |
boolean |
Plays each source once it opens |
loop |
boolean |
Restarts at the end |
audio |
boolean |
Opens the audio track. Defaults to true. Changing it reopens an open source |
muted |
boolean |
Silences audio. The audio clock keeps running |
volume |
number |
Non-negative and within the 32-bit float range. Defaults to 1 |
avSyncOffsetMs |
number |
From -60000 to 60000. Positive values show frames earlier relative to the audio |
maxFps |
number |
Integer presentation-rate cap from 1 to 120. Defaults to 30 |
ready |
boolean |
Whether a source is open |
videoMetadata |
VideoMetadata | null |
width, height, fps, duration, and hasAudio. Zero is unknown |
playing, paused |
boolean |
Playback state. Before a source opens, the requested state |
ended |
boolean |
Whether playback reached the end without looping |
currentTime |
number |
Media time in seconds. Assigning it seeks |
duration |
number |
Duration in seconds |
qualityTier |
number |
Adaptive decode-size step for Kitty output |
play(), pause() |
() => void |
Start or stop playback. toggle() switches |
seek(time) |
(seconds: number) => void |
Seek. seekBy(delta) and seekToMs(ms) are relative and millisecond |
| Callbacks | onReady, onError, onEnd, … |
onPlay, onPause, onSeek, and onTimeUpdate report changes |
Options set before a source opens apply when it opens. An option value out of range throws a RangeError and keeps the previous value. A crop that is not whole pixels with a positive size, or lies outside the open source, calls onError and keeps the previous crop. Each source checks the crop when it opens; until one fits, the video shows the whole frame.
Open, seek, and decode failures call onError with the native detail. A failed decode stops playback at its position and keeps the last frame; play() and seek() retry, and play() after a failed open opens the source again. An audio failure keeps playback running silently on the wall clock.
Related components#
Use Image for still images. See Audio for sound that is not tied to a video.