Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Elemente

Video

Ein Video-View spielt eine Videodatei oder einen Stream mit dem Player der Plattform ab: AVPlayer unter iOS und Mac Catalyst, MediaPlayer unter Android. Die Wiedergabe wird über Props gesteuert (paused, muted, rate, …), sodass der Player wie jede andere View deinem State folgt. In Rust erzeugst du einen mit video(url), in TSX mit <Video> aus 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 ist src erforderlich (siehe die Video API). Siehe auch den Leitfaden zu Web View und Video.

#Props

Ein Video übernimmt die Props einer View (es hat keine Kinder) sowie:

Prop Rust Typ Standard Beschreibung
src video(url) string Die URL des Videos (in Rust auch der uri eines Assets: video(CLIP.uri)).
paused .paused(..) boolean false Die Wiedergabe ist pausiert.
muted .muted(..) boolean false Kein Ton.
volume .volume(..) number 1 0..1.
rate .rate(..) number 1 Die Wiedergabegeschwindigkeit.
repeat .repeat(..) boolean false Spielt am Ende erneut von vorn ab.
controls .controls(..) boolean false Zeigt die Wiedergabesteuerung der Plattform an.
objectFit (Style), object-* Klassen .object_fit(ResizeMode::Contain) "cover" | "contain" | "fill" | "none" | "scale-down" "contain" Wie das Video in seine Box passt, wie object-fit in CSS (class="object-cover"). iOS, macOS und Android spielen "none" und "scale-down" als "contain" ab, Linux beide als "scale-down".
progressUpdateInterval .progress_interval(ms) number 250 Millisekunden zwischen onProgress-Events.

Gib dem Video seine Größe über seine Styles vor; seine Größe ist die, die seine Styles ihm geben. Nutze die von onLoad gemeldete Größe, um ein Seitenverhältnis festzulegen, wenn du die eigenen Proportionen des Videos verwenden möchtest.

#Events

Event Rust Payload Wann
onLoadStart .on_load_start keine Das Laden hat begonnen.
onLoad .on_load { duration, size } (VideoInfo: duration, size) Das Video ist bereit: seine Dauer in Sekunden und seine Größe in Pixeln.
onProgress .on_progress { currentTime, duration } (VideoProgress: current_time, duration) Alle progressUpdateInterval während der Wiedergabe (Sekunden).
onBuffer .on_buffer { isBuffering } (bool) Die Wiedergabe stockt wegen fehlender Daten / wird fortgesetzt.
onEnd .on_end keine Die Wiedergabe hat das Ende erreicht (mit repeat am Ende jeder Schleife).
onError .on_error { message, code? } (&str, die Meldung) Laden oder Wiedergabe ist fehlgeschlagen; code ist "ERR_ACCESS_DENIED" bei einer Quelle, die die Capabilities der App nicht erlauben.
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)
    })

#Ref-Methoden

Methode Rust Beschreibung
seek(seconds) seek(seconds) Springt zu einer Zeitposition.

#Capabilities

Wenn die App Capabilities deklariert, werden Videoquellen von den Hosts geladen, die network erlaubt; eine Quelle auf einem anderen Host zeigt eine leere View und meldet onError mit code "ERR_ACCESS_DENIED".

#Hinweise zu Plattformen

  • iOS zeichnet das Video in einem AVPlayerLayer oder mit controls in der System-Player-View; AVFoundation wird beim ersten Erstellen eines Videos geladen.
  • Android spielt mit MediaPlayer auf einer TextureView ab, mit einem MediaController für controls.