تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

Video

يشغّل عنصر العرض Video ملف فيديو أو بثًّا باستخدام مشغّل المنصة: AVPlayer على iOS وMac Catalyst، وMediaPlayer على Android. يتم التحكم في التشغيل عبر الخصائص (paused، muted، rate، …)، فيتبع المشغّل حالة تطبيقك مثل أي عنصر عرض آخر. في 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 خصائص العرض (وليس له عناصر فرعية) بالإضافة إلى ما يلي:

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
srcvideo(url)stringعنوان URL للفيديو (في 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)
    })

#دوال المرجع

الدالةRustالوصف
seek(seconds)seek(seconds)ينتقل إلى وقت محدد.

#القدرات

عندما يعلن التطبيق عن القدرات، تُحمَّل مصادر الفيديو من المضيفين الذين يسمح بهم network؛ أما المصدر على مضيف آخر فيعرض عنصر عرض فارغًا ويبلّغ عن onError مع code "ERR_ACCESS_DENIED".

#ملاحظات حول المنصات

  • ترسم iOS الفيديو داخل AVPlayerLayer، أو في عنصر عرض المشغّل الخاص بالنظام عند استخدام controls؛ ويتم تحميل AVFoundation عند إنشاء أول فيديو.
  • تشغّل Android الفيديو باستخدام MediaPlayer على TextureView، مع MediaController لـ controls.