Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

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.

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())),
        )
}

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ù:

PropRustTipoPredefinitoDescrizione
srcvideo(url)stringL’URL del video (in Rust anche il uri di un asset: video(CLIP.uri)).
paused.paused(..)booleanfalseLa riproduzione è in pausa.
muted.muted(..)booleanfalseNessun audio.
volume.volume(..)number1Da 0 a 1.
rate.rate(..)number1La velocità di riproduzione.
repeat.repeat(..)booleanfalseAlla fine riparte dall’inizio.
controls.controls(..)booleanfalseMostra 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)number250Millisecondi 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

EventoRustPayloadQuando
onLoadStart.on_load_startnessunoIl 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_endnessunoLa 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)
    })

#Metodi del ref

MetodoRustDescrizione
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 con controls; AVFoundation viene caricato la prima volta che viene creato un video.
  • Android riproduce con MediaPlayer su un TextureView, con un MediaController per controls.