Elementi
Video
Una video view riproduce un file video o uno stream con il player della piattaforma: AVPlayer su iOS e Mac Catalyst, MediaPlayer su Android. La riproduzione si controlla tramite props (paused, muted, rate, …), quindi il player segue il tuo stato come qualsiasi altra view. Rust ne crea una con video(url), TSX con <Video> da 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 è obbligatorio (vedi la Video API). Vedi la guida a web view e video.
#Props
Un video accetta le props di una view (non ha figli) più:
| Prop | Rust | Tipo | Predefinito | Descrizione |
|---|---|---|---|---|
src | video(url) | string | L’URL del video (in Rust anche il uri di un asset: video(CLIP.uri)). | |
paused | .paused(..) | boolean | false | La riproduzione è in pausa. |
muted | .muted(..) | boolean | false | Nessun audio. |
volume | .volume(..) | number | 1 | Da 0 a 1. |
rate | .rate(..) | number | 1 | La velocità di riproduzione. |
repeat | .repeat(..) | boolean | false | Alla fine riparte dall’inizio. |
controls | .controls(..) | boolean | false | Mostra i controlli di riproduzione della piattaforma. |
objectFit (style), classi object-* | .object_fit(ResizeMode::Contain) | "cover" | "contain" | "fill" | "none" | "scale-down" | "contain" | Come il video si adatta al suo riquadro, come object-fit di CSS (class="object-cover"). iOS, macOS e Android riproducono "none" e "scale-down" come "contain", Linux entrambi come "scale-down". |
progressUpdateInterval | .progress_interval(ms) | number | 250 | Millisecondi tra gli eventi onProgress. |
Dai al video una dimensione tramite i suoi stili; la sua dimensione è quella che gli stili gli assegnano. Usa la dimensione riportata da onLoad per impostare un rapporto d’aspetto quando vuoi le proporzioni originali del video.
#Eventi
| Evento | Rust | Payload | Quando |
|---|---|---|---|
onLoadStart | .on_load_start | nessuno | Il caricamento è iniziato. |
onLoad | .on_load | { duration, size } (VideoInfo: duration, size) | Il video è pronto: la sua durata in secondi e la sua dimensione in pixel. |
onProgress | .on_progress | { currentTime, duration } (VideoProgress: current_time, duration) | A ogni progressUpdateInterval durante la riproduzione (secondi). |
onBuffer | .on_buffer | { isBuffering } (bool) | La riproduzione si è bloccata in attesa di dati / è ripresa. |
onEnd | .on_end | nessuno | La riproduzione ha raggiunto la fine (con repeat, alla fine di ogni ciclo). |
onError | .on_error | { message, code? } (&str, il messaggio) | Il caricamento o la riproduzione sono falliti; code è "ERR_ACCESS_DENIED" per una sorgente che le capabilities dell’app non consentono. |
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)} />;#Metodi del ref
| Metodo | Rust | Descrizione |
|---|---|---|
seek(seconds) | seek(seconds) | Salta a un istante. |
#Capabilities
Quando l’app dichiara le capabilities, le sorgenti video vengono caricate dagli host che network consente; una sorgente su un altro host mostra una view vuota e segnala onError con code "ERR_ACCESS_DENIED".
#Note sulle piattaforme
- iOS disegna il video in un
AVPlayerLayer, oppure nella vista del player di sistema concontrols; AVFoundation viene caricato la prima volta che viene creato un video. - Android riproduce con MediaPlayer su un TextureView, con un MediaController per
controls.