Elemente
Video
Ein Video-View spielt eine Videodatei oder einen Stream mit dem Player der Plattform ab: AVPlayer unter iOS und Mac Catalyst, MediaPlayer unter Android. Die Wiedergabe wird über Props gesteuert (paused, muted, rate, …), sodass der Player wie jede andere View deinem State folgt. In Rust erzeugst du einen mit video(url), in TSX mit <Video> aus 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 ist src erforderlich (siehe die Video API). Siehe auch den Leitfaden zu Web View und Video.
#Props
Ein Video übernimmt die Props einer View (es hat keine Kinder) sowie:
| Prop | Rust | Typ | Standard | Beschreibung |
|---|---|---|---|---|
src |
video(url) |
string |
Die URL des Videos (in Rust auch der uri eines Assets: video(CLIP.uri)). |
|
paused |
.paused(..) |
boolean |
false |
Die Wiedergabe ist pausiert. |
muted |
.muted(..) |
boolean |
false |
Kein Ton. |
volume |
.volume(..) |
number |
1 |
0..1. |
rate |
.rate(..) |
number |
1 |
Die Wiedergabegeschwindigkeit. |
repeat |
.repeat(..) |
boolean |
false |
Spielt am Ende erneut von vorn ab. |
controls |
.controls(..) |
boolean |
false |
Zeigt die Wiedergabesteuerung der Plattform an. |
objectFit (Style), object-* Klassen |
.object_fit(ResizeMode::Contain) |
"cover" | "contain" | "fill" | "none" | "scale-down" |
"contain" |
Wie das Video in seine Box passt, wie object-fit in CSS (class="object-cover"). iOS, macOS und Android spielen "none" und "scale-down" als "contain" ab, Linux beide als "scale-down". |
progressUpdateInterval |
.progress_interval(ms) |
number |
250 |
Millisekunden zwischen onProgress-Events. |
Gib dem Video seine Größe über seine Styles vor; seine Größe ist die, die seine Styles ihm geben. Nutze die von onLoad gemeldete Größe, um ein Seitenverhältnis festzulegen, wenn du die eigenen Proportionen des Videos verwenden möchtest.
#Events
| Event | Rust | Payload | Wann |
|---|---|---|---|
onLoadStart |
.on_load_start |
keine | Das Laden hat begonnen. |
onLoad |
.on_load |
{ duration, size } (VideoInfo: duration, size) |
Das Video ist bereit: seine Dauer in Sekunden und seine Größe in Pixeln. |
onProgress |
.on_progress |
{ currentTime, duration } (VideoProgress: current_time, duration) |
Alle progressUpdateInterval während der Wiedergabe (Sekunden). |
onBuffer |
.on_buffer |
{ isBuffering } (bool) |
Die Wiedergabe stockt wegen fehlender Daten / wird fortgesetzt. |
onEnd |
.on_end |
keine | Die Wiedergabe hat das Ende erreicht (mit repeat am Ende jeder Schleife). |
onError |
.on_error |
{ message, code? } (&str, die Meldung) |
Laden oder Wiedergabe ist fehlgeschlagen; code ist "ERR_ACCESS_DENIED" bei einer Quelle, die die Capabilities der App nicht erlauben. |
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-Methoden
| Methode | Rust | Beschreibung |
|---|---|---|
seek(seconds) |
seek(seconds) |
Springt zu einer Zeitposition. |
#Capabilities
Wenn die App Capabilities deklariert, werden Videoquellen von den Hosts geladen, die network erlaubt; eine Quelle auf einem anderen Host zeigt eine leere View und meldet onError mit code "ERR_ACCESS_DENIED".
#Hinweise zu Plattformen
- iOS zeichnet das Video in einem
AVPlayerLayeroder mitcontrolsin der System-Player-View; AVFoundation wird beim ersten Erstellen eines Videos geladen. - Android spielt mit MediaPlayer auf einer TextureView ab, mit einem MediaController für
controls.