تتيح ميزة "نافذة ضمن النافذة" للمستخدمين مشاهدة الفيديوهات في نافذة عائمة (تكون دائمًا فوق النوافذ الأخرى) حتى يتمكّنوا من مواصلة مشاهدة المحتوى الذي يشاهدونه أثناء التفاعل مع مواقع إلكترونية أو تطبيقات أخرى.
باستخدام Picture-in-Picture Web API، يمكنك بدء وضع "صورة في صورة" والتحكّم فيه لعناصر الفيديو على موقعك الإلكتروني. يمكنك تجربة الميزة على عيّنة "نافذة ضمن النافذة" الرسمية.
الخلفية
في أيلول (سبتمبر) 2016، أضاف Safari ميزة "صورة في صورة" من خلال واجهة برمجة التطبيقات WebKit API في نظام التشغيل macOS Sierra. وبعد ستة أشهر، بدأ Chrome تشغيل ميزة "صورة في صورة" تلقائيًا للفيديوهات على الأجهزة الجوّالة مع إصدار Android O باستخدام واجهة برمجة تطبيقات Android الأصلية. وبعد ستة أشهر، أعلنّا عن نيّتنا إنشاء واجهة برمجة تطبيقات Web API متوافقة مع Safari ومحاولة توحيده، ما يتيح لمطوّري الويب إنشاء التجربة الكاملة حول ميزة "صورة في صورة" والتحكّم فيها. وها نحن ذا.
الدخول في الرمز
الدخول في وضع "نافذة ضمن النافذة"
لنبدأ ببساطة بعنصر فيديو وطريقة للمستخدم للتفاعل معه، مثل عنصر زر.
<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>
لا تطلب ميزة "نافذة ضمن النافذة" إلا استجابةً لإيماءة المستخدم، ولا تطلبها أبدًا في الوعد الذي يعرضه videoElement.play()
. ويرجع ذلك إلى أنّ الوعود لا تؤدي
إلى الآن إلى نشر إيماءات المستخدم. بدلاً من ذلك، يمكنك استدعاء requestPictureInPicture()
في معالج
النقر على pipButtonElement
كما هو موضّح أدناه. وتقع على عاتقك مسئولية التعامل
مع ما يحدث إذا نقر المستخدمون مرتين.
pipButtonElement.addEventListener('click', async function () {
pipButtonElement.disabled = true;
await videoElement.requestPictureInPicture();
pipButtonElement.disabled = false;
});
عند حلّ المشكلة، يقلّص Chrome الفيديو إلى نافذة صغيرة يمكن للمستخدم نقلها ووضعها فوق النوافذ الأخرى.
لقد انتهيتَ. أحسنت صنعًا. يمكنك التوقف عن القراءة والذهاب للاستمتاع برحلة مستحقّة. للأسف، هذا ليس هو الحال دائمًا. يجوز للجهة المانحة رفض الوعد لأيٍ مما يلي:
- لا يتيح النظام ميزة "صورة داخل صورة".
- لا يُسمح للمستند باستخدام ميزة "صورة في صورة" بسبب سياسة أذونات تقييدية.
- لم يتم تحميل البيانات الوصفية للفيديو بعد (
videoElement.readyState === 0
). - ملف الفيديو يتضمّن صوتًا فقط.
- سمة
disablePictureInPicture
الجديدة متوفّرة في عنصر الفيديو. - لم يتم إجراء المكالمة في معالِج حدث إيماءة المستخدم (مثل النقر على زر). بدءًا من الإصدار 74 من Chrome، ينطبق هذا فقط إذا لم يكن هناك عنصر في نافذة ضمن النافذة.
يعرض قسم دعم الميزات أدناه كيفية تفعيل/إيقاف زر استنادًا إلى هذه القيود.
لنضيف وحدة try...catch
لتسجيل هذه الأخطاء المحتمَلة وإعلام
المستخدم بما يحدث.
pipButtonElement.addEventListener('click', async function () {
pipButtonElement.disabled = true;
try {
await videoElement.requestPictureInPicture();
} catch (error) {
// TODO: Show error message to user.
} finally {
pipButtonElement.disabled = false;
}
});
ويعمل عنصر الفيديو بالطريقة نفسها سواء كان في نافذة "نافذة ضمن النافذة" أم لا: حيث يتم تنشيط الأحداث وإجراءات الاتصال. ويعرض هذا العنصر تغييرات الحالة في نافذة "نافذة ضمن النافذة" (مثل التشغيل والإيقاف المؤقت والتقديم/الترجيع وما إلى ذلك)، ومن الم possible أيضًا تغيير الحالة آليًا في JavaScript.
الخروج من وضع "نافذة ضمن نافذة"
الآن، لنجعل الزر يتيح تفعيل ميزة "نافذة ضمن النافذة" وإيقافها. علينا أولاً التحقّق مما إذا كان العنصر المقروء فقط document.pictureInPictureElement
هو عنصر الفيديو. وإذا لم يكن كذلك، سنرسل طلبًا للدخول إلى وضع
"صورة في صورة" كما هو موضّح أعلاه. في حال عدم رغبتك في ذلك، يُرجى مغادرة المكالمة من خلال الاتصال بالرقم document.exitPictureInPicture()
، ما يعني أنّ الفيديو سيظهر مجددًا في علامة التبويب الأصلية. تجدر الإشارة إلى أنّ هذه الطريقة تُرجع أيضًا وعدًا.
...
try {
if (videoElement !== document.pictureInPictureElement) {
await videoElement.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
}
...
الاستماع إلى أحداث "نافذة ضمن النافذة"
عادةً ما تحد أنظمة التشغيل من استخدام ميزة "صورة في صورة" في نافذة واحدة، لذلك يتّبع Chrome هذا النمط. وهذا يعني أنّه لا يمكن للمستخدمين تشغيل سوى فيديو واحد في وضع "نافذة ضمن النافذة" في كل مرة. يجب أن تتوقع أن يخرج المستخدمون من نافذة ضمن النافذة حتى عندما لا تطلب ذلك.
تتيح لنا معالجات الأحداث الجديدة enterpictureinpicture
وleavepictureinpicture
تخصيص التجربة للمستخدمين. يمكن أن يكون هذا المحتوى على شكل فيديوهات من مكتبة أو محادثة في بث مباشر.
videoElement.addEventListener('enterpictureinpicture', function (event) {
// Video entered Picture-in-Picture.
});
videoElement.addEventListener('leavepictureinpicture', function (event) {
// Video left Picture-in-Picture.
// User may have played a Picture-in-Picture video from a different page.
});
تخصيص نافذة ميزة "نافذة ضمن النافذة"
يتيح الإصدار 74 من Chrome استخدام أزرار التشغيل أو الإيقاف المؤقت، والمقطع الصوتي السابق، والمقطع الصوتي التالي في نافذة "نافذة ضمن النافذة" التي يمكنك التحكّم فيها باستخدام واجهة برمجة التطبيقات لجلسات الوسائط.
يظهر زر التشغيل أو الإيقاف المؤقت دائمًا ضمن النافذة "نافذة ضمن النافذة"
إلا إذا كان الفيديو يشغّل عناصر MediaStream (مثل getUserMedia()
أو getDisplayMedia()
أو canvas.captureStream()
) أو إذا تم ضبط مدة مصدر الوسائط على +Infinity
(مثل خلاصة البث المباشر). للتأكّد من أنّ زر التشغيل/الإيقاف المؤقت
يكون مرئيًا دائمًا، يمكنك ضبط بعض معالِجات إجراءات جلسة الوسائط لكلّ من حدثَي "التشغيل" و
"الإيقاف المؤقت" كما هو موضّح أدناه.
// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function () {
// User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function () {
// User clicked "Pause" button.
});
يُرجى العِلم أنّ عرض عناصر التحكّم في نافذتَي "الأغنية السابقة" و "الأغنية التالية" متشابه. يؤدي ضبط معالِجات إجراءات "جلسات الوسائط" لتلك الإعدادات إلى عرضها في نافذة "نافذة ضمن النافذة" وستتمكّن من معالجة هذه الإجراءات.
navigator.mediaSession.setActionHandler('previoustrack', function () {
// User clicked "Previous Track" button.
});
navigator.mediaSession.setActionHandler('nexttrack', function () {
// User clicked "Next Track" button.
});
للاطّلاع على ذلك، جرِّب نموذج جلسة الوسائط الرسمي.
الحصول على حجم نافذة "نافذة ضمن النافذة"
إذا أردت ضبط جودة الفيديو عند دخوله ميزة "نافذة ضمن النافذة" ومغادرته، عليك معرفة حجم نافذة "نافذة ضمن النافذة" وتلقّي إعلام إذا غيّر المستخدم حجم النافذة يدويًا.
يوضح المثال أدناه كيفية الحصول على عرض وارتفاع نافذة "نافذة ضمن النافذة" عند إنشائها أو تغيير حجمها.
let pipWindow;
videoElement.addEventListener('enterpictureinpicture', function (event) {
pipWindow = event.pictureInPictureWindow;
console.log(`> Window size is ${pipWindow.width}x${pipWindow.height}`);
pipWindow.addEventListener('resize', onPipWindowResize);
});
videoElement.addEventListener('leavepictureinpicture', function (event) {
pipWindow.removeEventListener('resize', onPipWindowResize);
});
function onPipWindowResize(event) {
console.log(
`> Window size changed to ${pipWindow.width}x${pipWindow.height}`
);
// TODO: Change video quality based on Picture-in-Picture window size.
}
أنصح بعدم الربط مباشرةً بحدث تغيير الحجم لأنّ كل تغيير صغير يتم إجراؤه على حجم نافذة "صورة في صورة" سيؤدي إلى بدء حدث منفصل قد يتسبب في مشاكل في الأداء إذا كنت تُجري عملية باهظة الثمن عند كل تغيير حجم. بعبارة أخرى، ستؤدي عملية تغيير الحجم إلى تشغيل الأحداث مراراً وتكراراً وبسرعة شديدة. أنصح باستخدام تقنيات شائعة، مثل تقييد السرعة وإزالة الارتداد، لحلّ هذه المشكلة.
إتاحة الميزات
قد لا تكون واجهة برمجة التطبيقات Picture-in-Picture Web API متوافقة، لذا عليك رصد ذلك
لتوفير ميزة "التحسين التدريجي". حتى في حال توفّر هذه الميزة، قد يوقفها
المستخدم أو توقِفها سياسة الأذونات. لحسن الحظ، يمكنك استخدام القيمة المنطقية الجديدة document.pictureInPictureEnabled
لتحديد ذلك.
if (!('pictureInPictureEnabled' in document)) {
console.log('The Picture-in-Picture Web API is not available.');
} else if (!document.pictureInPictureEnabled) {
console.log('The Picture-in-Picture Web API is disabled.');
}
عند تطبيقها على عنصر زر معيّن في فيديو، إليك كيفية التعامل مع مستوى ظهور زر "صورة في صورة".
if ('pictureInPictureEnabled' in document) {
// Set button ability depending on whether Picture-in-Picture can be used.
setPipButton();
videoElement.addEventListener('loadedmetadata', setPipButton);
videoElement.addEventListener('emptied', setPipButton);
} else {
// Hide button if Picture-in-Picture is not supported.
pipButtonElement.hidden = true;
}
function setPipButton() {
pipButtonElement.disabled =
videoElement.readyState === 0 ||
!document.pictureInPictureEnabled ||
videoElement.disablePictureInPicture;
}
إتاحة فيديو MediaStream
تتيح عناصر MediaStream التي تشغّل الفيديو (مثل getUserMedia()
وgetDisplayMedia()
canvas.captureStream()
) أيضًا وضع "صورة في صورة" في الإصدار 71 من Chrome. وهذا يعني أنّه يمكنك عرض نافذة "نافذة ضمن النافذة" تتضمّن الفيديو المضمّن من كاميرا الويب الخاصة بالمستخدم أو الفيديو الذي يتم عرضه أو حتى عنصر لوحة رسم. تجدر الإشارة إلى أنّه ليس من الضروري إرفاق عنصر
الفيديو بعنصر DOM للدخول إلى وضع
"صورة في صورة" كما هو موضّح أدناه.
عرض كاميرا الويب الخاصة بالمستخدم في نافذة "نافذة ضمن النافذة"
const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
video.play();
// Later on, video.requestPictureInPicture();
عرض الشاشة في نافذة "نافذة ضمن النافذة"
const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getDisplayMedia({video: true});
video.play();
// Later on, video.requestPictureInPicture();
عرض عنصر لوحة الرسم في نافذة "نافذة ضمن النافذة"
const canvas = document.createElement('canvas');
// Draw something to canvas.
canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);
const video = document.createElement('video');
video.muted = true;
video.srcObject = canvas.captureStream();
video.play();
// Later on, video.requestPictureInPicture();
من خلال الجمع بين canvas.captureStream()
وMedia Session API، يمكنك مثلاً إنشاء نافذة قائمة تشغيل صوتية في Chrome 74. يمكنك الاطّلاع على نموذج قائمة تشغيل صوتية الرسمي.
العيّنات والعروض التوضيحية والدروس التطبيقية حول الترميز
اطّلِع على نموذج ميزة "نافذة ضمن النافذة" الرسمي لتجربة واجهة برمجة التطبيقات لميزة "نافذة ضمن النافذة".
وستتوفّر العروض التوضيحية والدروس التطبيقية حول الترميز لاحقًا.
الخطوات التالية
أولاً، اطّلِع على صفحة حالة التنفيذ لمعرفة أجزاء واجهة برمجة التطبيقات التي تم تنفيذها حاليًا في Chrome والمتصفّحات الأخرى.
في ما يلي الميزات التي يمكنك توقّعها في المستقبل القريب:
- سيتمكّن مطوّرو الويب من إضافة عناصر تحكّم مخصّصة في ميزة "صورة في صورة".
- سيتم توفير واجهة برمجة تطبيقات ويب جديدة لعرض كائنات
HTMLElement
عشوائية في نافذة عائمة.
دعم المتصفح
تتوفّر واجهة برمجة التطبيقات Picture-in-Picture Web API في متصفّحات Chrome وEdge وOpera وSafari. راجِع MDN للحصول على التفاصيل.
الموارد
- حالة ميزات Chrome: https://www.chromestatus.com/feature/5729206566649856
- أخطاء تنفيذ Chrome: https://crbug.com/?q=component:Blink>Media>PictureInPicture
- مواصفات واجهة برمجة التطبيقات لميزة "نافذة داخل نافذة": https://wicg.github.io/picture-in-picture
- المشاكل المتعلقة بالمواصفات: https://github.com/WICG/picture-in-picture/issues
- نموذج: https://googlechrome.github.io/samples/picture-in-picture/
- رمز بديل غير رسمي لميزة "صورة داخل صورة": https://github.com/gbentaieb/pip-polyfill/
نشكر "منير لاموري" وجينيفر أبيسيلّي على عملهما على تطوير ميزة "صورة في صورة" ومساعدتهما في كتابة هذه المقالة. ونودّ أن نشكر جميع الأشخاص المشاركين في جهود توحيد المقاييس.