Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

É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.

el-video.tsx
el-video.tsxLive
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())),
        )
}

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 :

PropRustTypeValeur par défautDescription
srcvideo(url)stringL’URL de la vidéo (en Rust, le uri d’un asset convient aussi : video(CLIP.uri)).
paused.paused(..)booleanfalseLa lecture est en pause.
muted.muted(..)booleanfalseAucun son.
volume.volume(..)number1De 0 à 1.
rate.rate(..)number1La vitesse de lecture.
repeat.repeat(..)booleanfalseRelit depuis le début à la fin de la vidéo.
controls.controls(..)booleanfalseAffiche 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)number250Millisecondes 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énementRustCharge utileQuand
onLoadStart.on_load_startaucuneLe 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_endaucuneLa 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)
    })

#Méthodes de ref

MéthodeRustDescription
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 avec controls ; AVFoundation est chargé à la première création d’une vidéo.
  • Android lit avec MediaPlayer sur une TextureView, avec un MediaController pour controls.