هذه الصفحة مترجمة من الإنجليزية. اقرأ الصفحة الإنجليزية
يقدّم Zeyn ملفات الوسائط عبر طلبات النطاق في HTTP (range requests) ويشغّلها في المكوّنين Video وAudio. تتولى العنصران الأصليان <video> و<audio> فك الترميز، بينما يرسم Zeyn عناصر التحكم الخاصة به فوقهما بصورة قابلة للوصول.
تقديم الملفات
سجّل MediaSource على الميزة (Feature) مع صلاحية:
Feature::new("library")
.media("films", "library.watch", FilmFolder::new())
.media("music", "library.listen", SongTable::new())يجلب المتصفح GET /_zeyn/media/{source}/{id}. وقبل أن يرى المصدر أي شيء، يقوم الخادم بما يلي:
- يرفض المعرّفات التي ليست من 1 إلى 128 حرفًا من
A-Z a-z 0-9 - _ .، أو التي تبدأ بنقطة أو تحتوي على..(404)؛ - يتحقق من صلاحية المصدر (
403) ومن حراس الميزة (guards)؛ - يشغّل المعترضات (interceptors) (
CallKind::Media، مع اسم المصدر فيscreen).
تبحث open(ctx, id) عن المعرّف في فهرس التطبيق نفسه وتُرجع Media: إما Media::file(path) لملف على القرص (النوع من الامتداد، ووسم الكيان من الحجم ووقت التعديل)، أو Media::bytes(data, type) لبيانات في الذاكرة، أو Media::new(reader, len, type) لأي Read + Seek. المعرّف اسم وليس مسارًا أبدًا: ابنِ المسار من نتيجة البحث، لا من المعرّف.
الاستجابات:
| الطلب | الاستجابة |
|---|---|
بلا Range، أو وحدة أخرى، أو عدة نطاقات | 200 مع الملف كاملًا |
bytes=a-b أو bytes=a- أو bytes=-n | 206 مع Content-Range: bytes a-b/len |
| نطاق بايتات مشوّه أو يبدأ بعد النهاية | 416 مع Content-Range: bytes */len |
If-Range لا يطابق وسم الكيان | 200 مع الملف كاملًا |
If-None-Match يطابق وسم الكيان | 304 |
كل استجابة تحمل Accept-Ranges: bytes وCache-Control: private, no-cache وX-Content-Type-Options: nosniff. لا تُعرض ضمن الصفحة إلا ملفات الصوت والفيديو والصور النقطية وWebVTT وقوائم التشغيل المتدفقة؛ أما أي نوع آخر (HTML أو SVG أو السكربتات) فيُرسل بالنوع application/octet-stream مع Content-Disposition: attachment، فلا يمكن لملف أن يعمل كصفحة من التطبيق. تُقرأ الملفات على دفعات من 64 KiB في مجمّع الخيوط الحاجبة (blocking pool) مع أربع دفعات كحد أقصى في الطابور، فتبقى الذاكرة ثابتة مهما كان حجم الملف، ويوقف العميل الذي ينصرف القراءة. تنطبق حدود المعدل كما في بقية الطلبات؛ وقراءات الوسائط ليست عمليات كتابة.
لا تحتاج سياسة أمان المحتوى إلى أي تغيير: فالتوجيه default-src 'self' يسمح أصلًا بالوسائط من التطبيق. أما مكتبة البث التكيفي التي تستخدم Media Source Extensions فتحتاج إلى blob: في media-src، وهو ما لا يضيفه Zeyn.
الملفات العامة: الصور والشعارات والتنزيلات
الملفات التي يحق لأي شخص رؤيتها، سواء سجّل الدخول أم لا، توضع في مصدر عام:
Feature::new("catalog").public_media("images", 86_400, ImageFolder::new("data/images"))يتجاوز GET /_zeyn/media/images/{id} فحص الصلاحية (أما المعرّفات والحراس والمعترضات فتعمل كالمعتاد)، ولا يضبط أي ملف تعريف ارتباط (cookie)، ويجيب بالترويسة Cache-Control: public, max-age=86400 (بحد أقصى MAX_PUBLIC_MEDIA_CACHE، أي سنة واحدة)، فتحتفظ به المتصفحات وشبكة CDN أمام التطبيق. ويتيح وسم الكيان لها مع ذلك إعادة التحقق عبر 304. استخدم المسار قيمةً لـ src في Image. وفي حالة الصور الكبيرة الكثيرة يبقى مضيف صور منفصل خلف CDN الخيار الأرخص؛ اسمح به عبر App::csp(|c| c.img_src([...])).
- يجب أن تُرجع
openالبايتات نفسها لكل زائر. لا تغيّر ملفًا عامًا بحسب المستخدم أو الدور أو المستأجر (tenant) أو اللغة: فشبكة CDN أو الوكيل تحتفظ بالاستجابة الأولى وتسلّمها للجميع. الملفات التي تختلف من مستخدم إلى آخر مكانها مصدر مسجَّل عبرFeature::media. - مع قيمة
max_ageطويلة، أعطِ معرّفات تتغير بتغيّر المحتوى، مثل تجزئة المحتوى (poster-3f9a2c.jpg). فيحصل الملف المتغير على عنوان جديد، بدل أن تعيش النسخ القديمة في الذاكرات المؤقتة حتى تنتهي صلاحيتها. - لا تُحتسب قراءات المصادر العامة (
GETوHEAD) ضمنApp::rate_limit: فقد تحمّل صفحة واحدة عشرات الملصقات، ويشترك كثير من زوار الجوّال في عنوان واحد خلف NAT شركة الاتصالات. أما المصادر الخاصة فتُحتسب، وتبقى قواعد السماح والمنع لعناوين IP سارية على النوعين. والحدّ من إساءة استخدام عرض النطاق مهمة CDN أو الوكيل العكسي أمام التطبيق.
المشغّلات
Video::new("films", t!("films.harbor"))
.subtitle(t!("films.harbor_sub"))
.file("harbor.webm")
.file("harbor.mp4")
.hls("harbor.m3u8")
.poster("harbor.jpg")
.captions("harbor.en.vtt", "en", "English")
.captions("harbor.tr.vtt", "tr", "Türkçe")
Audio::new("music", t!("songs.dawn")).subtitle(t!("songs.artist")).file("dawn.mp3")عناصر التحكم: التشغيل والإيقاف المؤقت، ومنزلق التقدّم، والوقت، وكتم الصوت، ومنزلق مستوى الصوت، وقائمة سرعة التشغيل، وزر الترجمات النصية، و(عند وجود أكثر من ملف ترجمة) قائمة اللغات، وللفيديو صورة داخل صورة (حيث يدعمها المتصفح) وملء الشاشة. تتبع المنزلقات نمط WAI-ARIA للمنزلق: تحرّك الأسهم 5 ثوانٍ (أو 5 بالمئة من مستوى الصوت)، ويحرّك Page Up وPage Down عُشرًا، وينتقل Home وEnd إلى الطرفين. وتتبع القوائم نمط زر القائمة. وما دام التركيز داخل المشغّل، يقوم k أو Space بالتشغيل والإيقاف المؤقت، ويقفز j وl عشر ثوانٍ، ويكتم m الصوت، ويبدّل c الترجمات، ويبدّل f ملء الشاشة؛ ولا تفعل هذه المفاتيح شيئًا في بقية الصفحة.
الترجمات النصية ملفات WebVTT يقدّمها المصدر نفسه. يرسم المشغّل المقطع النشط نصًّا عاديًا (يُهمل وسم المقطع)، فوق الصورة في الفيديو وتحت العنوان في الصوت، فتبدو الترجمات متطابقة في كل متصفح وفي وضع ملء الشاشة.
الاستئناف
يحفظ المشغّل الموضع الذي توقف عنده المستخدم: كل 15 ثانية أثناء التشغيل، وعند الإيقاف المؤقت، وعند إخفاء الصفحة أو إغلاقها، و0 عند انتهاء الوسائط. ويستأنف حين يبعد الموضع المحفوظ أكثر من 5 ثوانٍ عن أي من الطرفين.
- عندما يطبّق المصدر
positionوsave_position، تذهب المواضع إلى الخادم: يجيبGET /_zeyn/media/position?source=&id=بـ{"at": seconds, "kept": true}، ويحفظPOST /_zeyn/media/positionمع{source, id, at}الموضع (204). كلاهما يتحقق من المعرّف والصلاحية والحراس ويحتاج إلى مستخدم مسجّل الدخول؛ ويحتاج الـ POST أيضًا إلى ترويسة CSRF ويُحتسب عملية كتابة في حدود المعدل. المواضع التي تتجاوزMAX_POSITION_SECONDSأو تقل عن الصفر تعطي422. - وإلا (أي الطرق الافتراضية للمصدر، أو مستخدم مجهول) تجيب القراءة بـ
{"at": null, "kept": false}ويحتفظ المشغّل بالموضع في التخزين المحلي للمتصفح تحت المفتاحzeyn.media.{source}/{id}. وتُتجاهل أخطاء التخزين (النوافذ الخاصة، أو التخزين المحجوب).
مواضع الخادم هي الخيار الأفضل للمستخدمين المسجّلين: فهي تتبع المستخدم إلى كل جهاز ويمكن للتطبيق قراءتها لعرض صف "تابع المشاهدة". أما التخزين المحلي فلا يحتاج إلى شيفرة لكنه يبقى في متصفح واحد ويشترك فيه كل من يستخدمه.
HLS وDASH
تضيف hls(id) وdash(id) بثًّا تكيفيًّا. يشغّل Safari الـ HLS أصلًا، فيستخدم قائمة التشغيل. أما المتصفحات الأخرى فتحتاج إلى مكتبة JavaScript (مثل hls.js أو dash.js) لا يشحنها Zeyn. وقبل الرجوع إلى الملفات التدريجية، يُطلق المشغّل حدثًا قابلًا للإلغاء هو zeyn:media-adaptive على جذره:
document.addEventListener('zeyn:media-adaptive', (e) => {
const { media, kind, url } = e.detail;
if (kind !== 'hls' || !window.Hls?.isSupported()) return;
e.preventDefault();
const hls = new window.Hls();
hls.loadSource(url);
hls.attachMedia(media);
});حمّل ذلك السكربت من ملف في تطبيقك (فالسكربتات المضمّنة محجوبة بسياسة أمان المحتوى). وعندما لا يستدعي أي مستمع preventDefault()، تُشغَّل مدخلات file بالترتيب. وتُقدَّم ملفات قائمة التشغيل والمقاطع من المصدر نفسه، فتعمل عناوينها النسبية ما دامت معرّفاتها مدرجة فيه.
الملفات المرفوعة
تُقدَّم الملفات المرفوعة عبر Form::file من خلال GET /_zeyn/files/{store}/{id} بالاستجابات نفسها الخاصة بملفات الوسائط (النطاقات، وسوم الكيان، nosniff، والعرض ضمن الصفحة للصور النقطية والصوت والفيديو فقط)، وذلك بعد صلاحية المخزن وحراس الميزة وفحص المستأجر. راجع قسم رفع الملفات في examples/docs/pages/forms.md.