Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

Video

A video view reproduce un archivo o flujo de vídeo con el reproductor de la plataforma: AVPlayer en iOS y Mac Catalyst, MediaPlayer en Android. La reproducción se controla mediante props (paused, muted, rate, …), de modo que el reproductor sigue tu estado como cualquier otra vista. Rust crea una con video(url), TSX con <Video> de 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 es obligatorio (consulta la Video API). Consulta la guía de web view y video.

#Props

Un video acepta las props de una vista (no tiene hijos) y además:

Prop Rust Tipo Valor por defecto Descripción
src video(url) string La URL del video (en Rust, también el uri de un asset: video(CLIP.uri)).
paused .paused(..) boolean false La reproducción está en pausa.
muted .muted(..) boolean false Sin sonido.
volume .volume(..) number 1 0..1.
rate .rate(..) number 1 La velocidad de reproducción.
repeat .repeat(..) boolean false Vuelve a reproducirse desde el principio al llegar al final.
controls .controls(..) boolean false Muestra los controles de reproducción de la plataforma.
objectFit (estilo), clases object-* .object_fit(ResizeMode::Contain) "cover" | "contain" | "fill" | "none" | "scale-down" "contain" Cómo se ajusta el video a su caja, igual que object-fit de CSS (class="object-cover"). iOS, macOS y Android reproducen "none" y "scale-down" como "contain", y Linux ambos como "scale-down".
progressUpdateInterval .progress_interval(ms) number 250 Milisegundos entre eventos onProgress.

Dale un tamaño al video mediante sus estilos; su tamaño es el que le dan sus estilos. Usa el tamaño que informa onLoad para establecer una relación de aspecto cuando quieras las proporciones propias del video.

#Eventos

Evento Rust Payload Cuándo
onLoadStart .on_load_start ninguno Comenzó la carga.
onLoad .on_load { duration, size } (VideoInfo: duration, size) El video está listo: su duración en segundos y su tamaño en píxeles.
onProgress .on_progress { currentTime, duration } (VideoProgress: current_time, duration) Cada progressUpdateInterval durante la reproducción (segundos).
onBuffer .on_buffer { isBuffering } (bool) La reproducción se detuvo por falta de datos / se reanudó.
onEnd .on_end ninguno La reproducción llegó al final (con repeat, al final de cada repetición).
onError .on_error { message, code? } (&str, el mensaje) Falló la carga o la reproducción; code es "ERR_ACCESS_DENIED" para una fuente que las capabilities de la app no permiten.
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étodos de ref

Método Rust Descripción
seek(seconds) seek(seconds) Salta a un instante.

#Capabilities

Cuando la app declara capabilities, las fuentes de video se cargan desde los hosts que network permite; una fuente en otro host muestra una vista vacía y notifica onError con code "ERR_ACCESS_DENIED".

#Notas por plataforma

  • iOS dibuja el video en un AVPlayerLayer, o en la vista de reproductor del sistema con controls; AVFoundation se carga la primera vez que se crea un video.
  • Android reproduce con MediaPlayer sobre un TextureView, con un MediaController para controls.