Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Video

Video view воспроизводит видеофайл или поток штатным плеером платформы: AVPlayer на iOS и Mac Catalyst, MediaPlayer на Android. Воспроизведением управляют через свойства (paused, muted, rate, …), поэтому плеер следует за вашим состоянием, как и любой другой view. В Rust его создают с помощью video(url), в TSX — с помощью <Video> из 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())),
        )
}

В TSX src обязателен (см. Video API). См. также руководство по web view и video.

#Свойства

Video принимает свойства view (дочерних элементов у него нет), а также:

СвойствоRustТипПо умолчаниюОписание
srcvideo(url)stringURL видео (в Rust также uri ресурса: video(CLIP.uri)).
paused.paused(..)booleanfalseВоспроизведение приостановлено.
muted.muted(..)booleanfalseБез звука.
volume.volume(..)number1От 0 до 1.
rate.rate(..)number1Скорость воспроизведения.
repeat.repeat(..)booleanfalseПо окончании воспроизводит видео заново с начала.
controls.controls(..)booleanfalseПоказывает штатные элементы управления воспроизведением платформы.
objectFit (стиль), классы object-*.object_fit(ResizeMode::Contain)"cover" | "contain" | "fill" | "none" | "scale-down""contain"Как видео вписывается в свой блок, аналогично object-fit в CSS (class="object-cover"). iOS, macOS и Android воспроизводят "none" и "scale-down" как "contain", Linux — оба как "scale-down".
progressUpdateInterval.progress_interval(ms)number250Интервал в миллисекундах между событиями onProgress.

Размер видео задаётся через его стили: он равен тому, который указан в стилях. Если нужны собственные пропорции видео, используйте размер, который сообщает onLoad, чтобы задать соотношение сторон.

#События

СобытиеRustДанныеКогда
onLoadStart.on_load_startнетНачалась загрузка.
onLoad.on_load{ duration, size } (VideoInfo: duration, size)Видео готово: его длительность в секундах и размер в пикселях.
onProgress.on_progress{ currentTime, duration } (VideoProgress: current_time, duration)Каждые progressUpdateInterval во время воспроизведения (в секундах).
onBuffer.on_buffer{ isBuffering } (bool)Воспроизведение остановилось в ожидании данных / возобновилось.
onEnd.on_endнетВоспроизведение достигло конца (при repeat — в конце каждого цикла).
onError.on_error{ message, code? } (&str, сообщение)Не удалось загрузить или воспроизвести видео; code равно "ERR_ACCESS_DENIED" для источника, который не разрешён возможностями приложения.
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

МетодRustОписание
seek(seconds)seek(seconds)Переходит к указанному времени.

#Capabilities

Если приложение объявляет capabilities, источники видео загружаются с хостов, которые разрешает network; источник на другом хосте отображается как пустой view и сообщает onError с code "ERR_ACCESS_DENIED".

#Особенности платформ

  • iOS отображает видео в AVPlayerLayer или, при controls, в системном представлении плеера; AVFoundation загружается при первом создании видео.
  • Android воспроизводит видео с помощью MediaPlayer на TextureView, а для controls использует MediaController.