العناصر
Video
يشغّل عنصر العرض Video ملف فيديو أو بثًّا باستخدام مشغّل المنصة: AVPlayer على iOS وMac Catalyst، وMediaPlayer على Android. يتم التحكم في التشغيل عبر الخصائص (paused، muted، rate، …)، فيتبع المشغّل حالة تطبيقك مثل أي عنصر عرض آخر. في Rust يُنشأ باستخدام video(url)، وفي TSX باستخدام <Video> من nsh.
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())),
)
}import { signal, Text, Video, type VideoRef, View } from "nsh";
function Player() {
const paused = signal(true);
const time = signal("");
let video!: VideoRef;
return (
<View style={{ gap: 12 }}>
<Video
ref={video}
src="https://example.com/clip.mp4"
paused={paused()}
repeat
style={{ height: 220, backgroundColor: "#000000" }}
onProgress={(e) => time.set(`${e.currentTime.toFixed(1)} / ${e.duration.toFixed(1)} s`)}
/>
<View style={{ flexDirection: "row", gap: 16 }}>
<Text onPress={() => paused.set(!paused())}>{paused() ? "Play" : "Pause"}</Text>
<Text onPress={() => video.seek(0)}>Restart</Text>
<Text>{time()}</Text>
</View>
</View>
);
}في TSX يكون src مطلوبًا (راجع Video API). راجع دليل web view وvideo.
#الخصائص
يقبل عنصر Video خصائص العرض (وليس له عناصر فرعية) بالإضافة إلى ما يلي:
| الخاصية | Rust | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|---|
src | video(url) | string | عنوان URL للفيديو (في Rust يمكن أيضًا استخدام uri لأحد الأصول: video(CLIP.uri)). | |
paused | .paused(..) | boolean | false | التشغيل متوقف مؤقتًا. |
muted | .muted(..) | boolean | false | بدون صوت. |
volume | .volume(..) | number | 1 | من 0 إلى 1. |
rate | .rate(..) | number | 1 | سرعة التشغيل. |
repeat | .repeat(..) | boolean | false | يعيد التشغيل من البداية عند الوصول إلى النهاية. |
controls | .controls(..) | boolean | false | يعرض عناصر التحكم في التشغيل الخاصة بالمنصة. |
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) | number | 250 | المللي ثانية بين أحداث 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)
})const ratio = signal(16 / 9);
<Video src={url} style={{ width: "100%", aspectRatio: ratio() }} onLoad={(e) => ratio.set(e.size.width / e.size.height)} />;#دوال المرجع
| الدالة | Rust | الوصف |
|---|---|---|
seek(seconds) | seek(seconds) | ينتقل إلى وقت محدد. |
#القدرات
عندما يعلن التطبيق عن القدرات، تُحمَّل مصادر الفيديو من المضيفين الذين يسمح بهم network؛ أما المصدر على مضيف آخر فيعرض عنصر عرض فارغًا ويبلّغ عن onError مع code "ERR_ACCESS_DENIED".
#ملاحظات حول المنصات
- ترسم iOS الفيديو داخل
AVPlayerLayer، أو في عنصر عرض المشغّل الخاص بالنظام عند استخدامcontrols؛ ويتم تحميل AVFoundation عند إنشاء أول فيديو. - تشغّل Android الفيديو باستخدام MediaPlayer على TextureView، مع MediaController لـ
controls.