Éléments
Vidéo
Une vue vidéo lit un fichier ou un flux vidéo avec le lecteur de la plateforme : AVPlayer sur iOS et Mac Catalyst, MediaPlayer sur
Android. La lecture est contrôlée par des props (paused, muted, rate, …), de sorte que le lecteur suit votre état comme n’importe quelle
autre vue. Rust en crée une avec video(url), TSX avec <Video> depuis 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>
);
}En TSX, src est requis (voir l’API Video). Voir le
guide de la vue web et de la vidéo.
#Props
Une vidéo accepte les props d’une vue (elle n’a pas d’enfants) ainsi que :
| Prop | Rust | Type | Valeur par défaut | Description |
|---|---|---|---|---|
src | video(url) | string | L’URL de la vidéo (en Rust, le uri d’un asset convient aussi : video(CLIP.uri)). | |
paused | .paused(..) | boolean | false | La lecture est en pause. |
muted | .muted(..) | boolean | false | Aucun son. |
volume | .volume(..) | number | 1 | De 0 à 1. |
rate | .rate(..) | number | 1 | La vitesse de lecture. |
repeat | .repeat(..) | boolean | false | Relit depuis le début à la fin de la vidéo. |
controls | .controls(..) | boolean | false | Affiche les contrôles de lecture de la plateforme. |
objectFit (style), classes object-* | .object_fit(ResizeMode::Contain) | "cover" | "contain" | "fill" | "none" | "scale-down" | "contain" | Comment la vidéo s’adapte à sa boîte, comme le object-fit de CSS (class="object-cover"). iOS, macOS et Android lisent "none" et "scale-down" comme "contain", Linux les deux comme "scale-down". |
progressUpdateInterval | .progress_interval(ms) | number | 250 | Millisecondes entre les événements onProgress. |
Donnez une taille à la vidéo via ses styles ; sa taille est celle que ses styles lui donnent. Utilisez la taille que onLoad indique pour définir
un rapport d’aspect lorsque vous voulez conserver les proportions propres à la vidéo.
#Événements
| Événement | Rust | Charge utile | Quand |
|---|---|---|---|
onLoadStart | .on_load_start | aucune | Le chargement a commencé. |
onLoad | .on_load | { duration, size } (VideoInfo : duration, size) | La vidéo est prête : sa durée en secondes et sa taille en pixels. |
onProgress | .on_progress | { currentTime, duration } (VideoProgress : current_time, duration) | Tous les progressUpdateInterval pendant la lecture (en secondes). |
onBuffer | .on_buffer | { isBuffering } (bool) | La lecture est interrompue faute de données / a repris. |
onEnd | .on_end | aucune | La lecture a atteint la fin (avec repeat, à la fin de chaque boucle). |
onError | .on_error | { message, code? } (&str, le message) | Le chargement ou la lecture a échoué ; code vaut "ERR_ACCESS_DENIED" pour une source que les capabilities de l’app n’autorisent pas. |
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)} />;#Méthodes de ref
| Méthode | Rust | Description |
|---|---|---|
seek(seconds) | seek(seconds) | Saute à un instant donné. |
#Capabilities
Lorsque l’app déclare des capabilities, les sources vidéo se chargent depuis les hôtes que network
autorise ; une source sur un autre hôte affiche une vue vide et signale onError avec code "ERR_ACCESS_DENIED".
#Notes par plateforme
- iOS dessine la vidéo dans un
AVPlayerLayer, ou dans la vue de lecteur système aveccontrols; AVFoundation est chargé à la première création d’une vidéo. - Android lit avec MediaPlayer sur une TextureView, avec un MediaController pour
controls.