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, set audio: false on 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 when loop is on.
  • Frames draw like an Image with protocol: "auto". fit and crop work 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. qualityTier reports the step; 0 is full size.
  • Set OPENTUI_EXPERIMENTAL_VIDEO_HWACCEL=1 to 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.

Use Image for still images. See Audio for sound that is not tied to a video.

Changes0.8.1
0.8.1
core: VideoRenderable plays an MP4 file with H.264 video and AAC audio, with play(), pause(), seek(), loop, volume, maxFps, and callbacks such as onReady and onEnd. With audio: false, it opens no audio device and follows the wall clock. (#1800, #1884, #1883, #1885, #1887, #1889)
react: <video> renders a VideoRenderable, and VideoProps types its props. (#1800, #1884)
solid: <video> renders a VideoRenderable, and VideoProps types its props. (#1800, #1884)
core: ImageRenderable and VideoRenderable take crop: { left, top, width, height } in source pixels, and fit applies to that region. An invalid crop calls onError and keeps the previous crop. See Image and Video. (#1888)
native: The native library statically links decode-only LGPL FFmpeg 9.0.2 and zlib, so each stripped library is 2.6 to 3.5 MB larger. The packages ship the license, source, and relink notices. (#1800)
Added VideoRenderable, VideoMetadata, VideoRenderableOptions.