요소
Video
A video view plays a video file or stream with the platform’s player: AVPlayer on iOS and Mac Catalyst, MediaPlayer on
Android. Playback is controlled with props (paused, muted, rate, …), so the player follows your state like any
other view. Rust makes one with video(url), TSX with <Video> from nsh.
import { mount, signal, Text, Video, type VideoRef, View } from "nsh";
import { windowSize } from "nsh";
import { button, buttonText, ui } from "./palette";
// A short CC0 clip from MDN's examples.
const FLOWER = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4";
function Toggle(props: { label: string; on: boolean; onPress: () => void }) {
return (
<View
onPress={props.onPress}
{...button("secondary", { on: () => props.on, size: "sm" })}
>
<Text style={buttonText("secondary", { on: () => props.on })}>{props.label}</Text>
</View>
);
}
function App() {
const paused = signal(true);
const muted = signal(true);
const repeat = signal(true);
const controls = signal(false);
const fast = signal(false);
const time = signal(0);
const duration = signal(0);
const ratio = signal(16 / 9);
const status = signal("loading");
const ends = signal(0);
let video!: VideoRef;
const wide = () => windowSize().width >= 700;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>video</Text>
<View style={{ flex: 1, flexDirection: wide() ? "row" : "column", gap: 16 }}>
<View style={{ flex: wide() ? 1 : undefined, justifyContent: "center" }}>
<Video
ref={video}
src={FLOWER}
paused={paused()}
muted={muted()}
repeat={repeat()}
controls={controls()}
rate={fast() ? 2 : 1}
progressUpdateInterval={100}
style={{ width: "100%", aspectRatio: ratio(), borderRadius: 14, overflow: "hidden", backgroundColor: "#000000" }}
onLoadStart={() => status.set("onLoadStart")}
onLoad={(e) => {
duration.set(e.duration);
ratio.set(e.size.width / e.size.height);
status.set(`onLoad ${e.size.width} × ${e.size.height}`);
}}
onProgress={(e) => time.set(e.currentTime)}
onBuffer={(e) => status.set(e.isBuffering ? "buffering" : "playing")}
onEnd={() => ends.update((n) => n + 1)}
onError={(e) => status.set(`onError ${e.message}`)}
/>
</View>
<View style={{ gap: 12, width: wide() ? 300 : undefined, justifyContent: "center" }}>
<View style={{ ...ui().inset, height: 6, borderRadius: 3, overflow: "hidden" }}>
<View style={{ height: 6, width: `${duration() ? (time() / duration()) * 100 : 0}%`, backgroundColor: ui().accent }} />
</View>
<Text style={{ color: ui().text, fontFamily: "monospace" }}>
onProgress {time().toFixed(1)} / {duration().toFixed(1)} s
</Text>
<Text style={{ color: ui().muted, fontFamily: "monospace", fontSize: 13 }}>
{status()} · onEnd {ends()}×
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
<Toggle label={paused() ? "Play" : "Pause"} on={!paused()} onPress={() => paused.set(!paused())} />
<Toggle label="muted" on={muted()} onPress={() => muted.set(!muted())} />
<Toggle label="repeat" on={repeat()} onPress={() => repeat.set(!repeat())} />
<Toggle label="rate 2" on={fast()} onPress={() => fast.set(!fast())} />
<Toggle label="controls" on={controls()} onPress={() => controls.set(!controls())} />
<Toggle label="seek(0)" on={false} onPress={() => video.seek(0)} />
</View>
</View>
</View>
</View>
);
}
mount(App);use nsh_core::prelude::*;
fn player() -> impl IntoElement {
let paused = signal(true);
let time = signal(String::new());
let player = element_ref();
view()
.gap_3()
.child(
video("https://example.com/clip.mp4")
.element_ref(&player)
.paused(paused)
.repeat(true)
.object_fit(ResizeMode::Contain)
.h(px(220.0))
.bg(black())
.on_progress(move |p: &VideoProgress| {
time.set(format!("{:.1} / {:.1} s", p.current_time, p.duration))
}),
)
.child(
view()
.flex_row()
.gap_4()
.child(
text(move || if paused.get() { "Play" } else { "Pause" })
.on_press(move || paused.update(|p| *p = !*p)),
)
.child(text("Restart").on_press(move || player.seek(0.0)))
.child(text(move || time.get())),
)
}import { signal, Text, Video, type VideoRef, View } from "nsh";
function Player() {
const paused = signal(true);
const time = signal("");
let video!: VideoRef;
return (
<View style={{ gap: 12 }}>
<Video
ref={video}
src="https://example.com/clip.mp4"
paused={paused()}
repeat
style={{ height: 220, backgroundColor: "#000000" }}
onProgress={(e) => time.set(`${e.currentTime.toFixed(1)} / ${e.duration.toFixed(1)} s`)}
/>
<View style={{ flexDirection: "row", gap: 16 }}>
<Text onPress={() => paused.set(!paused())}>{paused() ? "Play" : "Pause"}</Text>
<Text onPress={() => video.seek(0)}>Restart</Text>
<Text>{time()}</Text>
</View>
</View>
);
}In TSX src is required (see the Video API). See the
web view and video guide.
#Props
A video takes the props of a view (it has no children) plus:
| Prop | Rust | Type | Default | Description |
|---|---|---|---|---|
src | video(url) | string | The video’s URL (in Rust, an asset’s uri too: video(CLIP.uri)). | |
paused | .paused(..) | boolean | false | Playback is paused. |
muted | .muted(..) | boolean | false | No sound. |
volume | .volume(..) | number | 1 | 0..1. |
rate | .rate(..) | number | 1 | The playback speed. |
repeat | .repeat(..) | boolean | false | Plays again from the start at the end. |
controls | .controls(..) | boolean | false | Shows the platform’s playback controls. |
objectFit (style), object-* classes | .object_fit(ResizeMode::Contain) | "cover" | "contain" | "fill" | "none" | "scale-down" | "contain" | How the video fits its box, as CSS’s object-fit (class="object-cover"). iOS, macOS and Android play "none" and "scale-down" as "contain", Linux both as "scale-down". |
progressUpdateInterval | .progress_interval(ms) | number | 250 | Milliseconds between onProgress events. |
Give the video a size through its styles; its size is the one its styles give it. Use the size onLoad reports to set
an aspect ratio when you want the video’s own proportions.
#Events
| Event | Rust | Payload | When |
|---|---|---|---|
onLoadStart | .on_load_start | none | Loading began. |
onLoad | .on_load | { duration, size } (VideoInfo: duration, size) | The video is ready: its duration in seconds and its size in pixels. |
onProgress | .on_progress | { currentTime, duration } (VideoProgress: current_time, duration) | Every progressUpdateInterval while playing (seconds). |
onBuffer | .on_buffer | { isBuffering } (bool) | Playback stalled for data / resumed. |
onEnd | .on_end | none | Playback reached the end (with repeat, at the end of each loop). |
onError | .on_error | { message, code? } (&str, the message) | Loading or playback failed; code is "ERR_ACCESS_DENIED" for a source the app’s capabilities do not grant. |
let ratio = signal(16.0_f32 / 9.0);
video(url)
.w_full()
.aspect_ratio(ratio)
.on_load(move |info: &VideoInfo| {
ratio.set(info.size.width.0 as f32 / info.size.height.0 as f32)
})const ratio = signal(16 / 9);
<Video src={url} style={{ width: "100%", aspectRatio: ratio() }} onLoad={(e) => ratio.set(e.size.width / e.size.height)} />;#Ref methods
| Method | Rust | Description |
|---|---|---|
seek(seconds) | seek(seconds) | Jumps to a time. |
#Capabilities
When the app declares capabilities, video sources load from the hosts network
allows; a source on another host shows an empty view and reports onError with code "ERR_ACCESS_DENIED".
#Platform notes
- iOS draws the video in an
AVPlayerLayer, or in the system player view withcontrols; AVFoundation is loaded the first time a video is created. - Android plays with MediaPlayer on a TextureView, with a MediaController for
controls.