G
YouTube TikTok Games
Guide · v0.12.0

Audio and video

<audio> and <video> behave like HTMLMediaElements, but the native runtime plays them: the platform decoder for pictures, a native mixer for sound, no browser underneath.

Video

player.tsx
import { Window, createApp } from "@tarve/core";

let size = "";

const app = createApp(() => (
  <Window title="Player" width={1100} height={720}>
    <video id="film" src="https://example.com/film.mp4" autoPlay muted loop controls fit="cover"
      style={{ width: "100%", height: "100%", radius: 18, background: "#000" }}
      onLoadedMetadata={info => { size = `${info.videoWidth}×${info.videoHeight}`; }} />
  </Window>
));

The picture fills the element's box like an image; fit is CSS object-fit (contain by default, cover, fill, none). UI placed over the video with position: "absolute" is drawn on top of it. With controls the element renders a default control bar (play/pause, time, seek bar, mute); without it, an <audio> renders nothing, as in a browser.

PlatformDecoder
Windows, D3D11Media Foundation decodes on the GPU into textures the renderer samples directly: no conversion pass, no copy. In a visible window the video gets its own composition layer, so new pictures repaint no UI.
Windows, CPU / wgpuMedia Foundation's Media Engine scales and converts each picture on the GPU at the size it is shown.
LinuxGStreamer (playbin, loaded at run time; install the codecs you need).
AndroidThe platform MediaPlayer with hardware decoders.
macOSNot yet: a <video> reports an error event and lays out as an empty box.

Audio

<audio> (and the sound of a <video> on the D3D11 path) is decoded by Symphonia and mixed into the default output (WASAPI on Windows, ALSA on Linux). When the output device goes away or the default changes, playback continues from the same sample on the new device. Seeks never block the window.

music.tsx
<audio id="song" src={track.url} autoPlay volume={0.8} muted={muted} loop={repeat}
  onPlay={() => { playing = true; }}
  onPause={() => { playing = false; }}
  onTimeUpdate={info => { time = info.currentTime; }}
  onEnded={next}
  onError={message => showError(message)} />

Sources

src takes a relative or absolute path, a file:// URL, a data: URL or an http(s):// URL. Remote files stream like a browser's: playback starts with the first chunk and the rest downloads behind it with HTTP range requests (one connection, 4 MB ranges, retries with backoff on 403, 429, 5xx and network errors). A stream whose network stalls for 30 s reports an error instead of ending as if it were over, so the app can reload it.

app.tsx
// embedded in the executable by tarve build, extracted once on first launch
import film from "./film.mp4" with { type: "file" };

<video src={film} controls />

tarve build only compiles the media subsystem when the app uses <audio> or <video>.

Props, events and control

PropMeaning
srcThe file or stream. Changing it loads the new one.
autoPlay, loop, mutedAs in HTML.
volume0–1.
controlsRenders the default control bar.
fit<video> only: contain, cover, fill, none.
onLoadedMetadata, onPlay, onPause, onTimeUpdate, onSeeking, onSeeked, onEndedReceive { currentTime, duration, paused, videoWidth, videoHeight }. timeupdate fires every 250 ms while playing.
timeA signal() the playback position (seconds) is written into on every report, without rendering the view. Bind it to a <Text> or a <Slider value={signal}> for a clock and a seek bar that cost no render while playing.
timeA signal() the playback position (seconds) is written into on every report, without rendering the view. Bind it to a <Text> or to <Slider value={position}> for a clock and a seek bar that cost no render while playing.
playbackRate0.0625–16 (1 by default): faster or slower at the same pitch. A new src starts at the prop's rate.
onError(message, { code })The file could not open, decode or keep streaming. code is network (HTTP status, host, timeout), format (a format or codec the system does not play), decode or aborted: skip to another source at once on network/format.
onWaiting, onCanPlay, onProgressBuffering: playback stopped for want of data, can play again, the download moved on. The default controls show their spinner while waiting.
onAudioTracksChange, onTextTracksChange, onCueChangeThe file's audio streams, its subtitle streams, and the embedded subtitle shown now.
subtitles, subtitle, subtitleStyle<video> only: external WebVTT/SRT files and the index shown (null: none), drawn over the picture.

Every event handler receives a MediaEventInfo:

FieldMeaning
currentTime, duration, pausedSeconds; duration is null until known.
videoWidth, videoHeightThe picture's own size.
bufferedEnd (s) of the downloaded range holding the current time: a buffer bar's end.
waitingOut of data while playing (between onWaiting and onCanPlay).
seekingA seek is under way (between onSeeking and onSeeked).
filenameThe served file's name after redirects (as mpv's), from loadedmetadata.
audioTracks, textTracks{ id, label, language, enabled }[]; audio tracks also carry codec (aac, ac3, eac3, dts…) and, on Windows with the D3D11 renderer, playable (false: no decoder on this machine, it would play silent; a first track that is not playable gives way to one that is): the streams inside the file.
cueTextThe embedded subtitle shown now; empty when none.

Give the element an id and drive it imperatively with app.media(id):

app.tsx
const player = app.media("song");
player.play();
player.pause();
player.seek(42);        // seconds
player.setVolume(0.5);
player.setMuted(true);
player.setPlaybackRate(1.25); // same pitch
player.setAudioTrack(id);    // from info.audioTracks
player.setTextTrack(id);     // from info.textTracks; null hides

Speed, tracks and subtitles

player.tsx
let audio: MediaAudioTrack[] = [], texts: MediaTextTrack[] = [];

<video id="film" src={episode.url} controls playbackRate={speed}
  subtitles={[{ src: "pt-BR.vtt", label: "Português" }]} subtitle={showExternal ? 0 : null}
  onAudioTracksChange={info => { audio = info.audioTracks; app.update(); }}
  onTextTracksChange={info => { texts = info.textTracks; app.update(); }}
  onError={(message, { code }) => { if (code === "network" || code === "format") nextSource(); }} />

// a menu item
app.media("film").setAudioTrack(audio[1].id);
app.media("film").setTextTrack(texts[0].id);   // null: none
app.media("film").setPlaybackRate(1.5);

Playback rate keeps the pitch everywhere: a WSOLA time stretch for <audio> and the D3D11 video path, the Media Engine's rate on the other Windows path, a rate seek on GStreamer (scaletempo) and PlaybackParams on Android.

Audio tracks are the file's audio streams (MKV, MP4). Switching keeps the position. Labels and languages come from the file; on Linux an unnamed stream is "Audio 1", "Audio 2"…

Subtitles come two ways. subtitles loads external WebVTT or SubRip files (path or URL); the cue under the current time is drawn over the picture, on time between timeupdates. Text streams inside the file are listed in textTracks: on Windows they are read from the container (Matroska S_TEXT SRT/ASS/WebVTT for local files, MP4 tx3g/wvtt local or remote), on Android from MediaPlayer's timed text, and their cues reach cueText and the overlay; GStreamer draws the chosen stream into the picture itself. Picture subtitles (PGS, VobSub) are not shown.

Plain http:// on Android is blocked unless the app sets tarve.android.cleartextTraffic: true in package.json (see Platform); https:// always works.

Buffering: a stalled download reports waiting and then canplay. An <audio> plays silence meanwhile instead of blocking the mix, and its clock stops. buffered follows the app's own download for remote files, GStreamer's buffering query and MediaPlayer's buffering percentage.

Seeking: seeking fires when a seek starts and seeked when the picture at the target shows; a seek still downloading its target while playing also reports waiting (then canplay), so a spinner can follow info.waiting || info.seeking. Seeks asked meanwhile collapse into the last one, which runs once the one in flight finishes.

HLS: an .m3u8 URL plays on Windows 10+ through the Media Engine (with either renderer).

Media keys and background playback

app.systemMedia shows the current track in the system media controls and forwards play, pause, next, previous and seek, hardware media keys included. Pass null to clear it. appIdentity: { id, name, icon? } in createApp names the app there instead of the host executable.

music.tsx
app.systemMedia({
  title: song.title, artist: song.artist, album: song.album, artwork: song.cover,
  playing, duration, position: time, canSeek: true, canNext: true, canPrevious: true,
  onAction: (action, time) => {
    if (action === "play") app.media("song").play();
    else if (action === "pause") app.media("song").pause();
    else if (action === "next") next();
    else if (action === "previous") previous();
    else if (time !== undefined) app.media("song").seek(time);
  },
});

Windows shows it in the media flyout and on the lock screen. On Android, while media plays the app keeps running in the background as a foreground service with a media notification, lock-screen and headset controls; systemMedia adds the title, artwork and next/previous. The repository's examples/video, examples/player and examples/youtube-music are complete players.