مشاهدة الفيديو باستخدام ميزة "نافذة ضمن النافذة"

François Beaufort
François Beaufort

تتيح ميزة "نافذة ضمن النافذة" للمستخدمين مشاهدة الفيديوهات في نافذة عائمة. (دائمًا فوق النوافذ الأخرى) حتى يتمكنوا من مراقبة ما يشاهدونها أثناء التفاعل مع مواقع أو تطبيقات أخرى.

باستخدام واجهة برمجة تطبيقات الويب لميزة "نافذة ضمن النافذة"، يمكنك بدء التشغيل أو التحكّم في نافذة ضمن النافذة لعناصر الفيديو على موقعك الإلكتروني جرِّبه على نموذج "نافذة ضمن النافذة" الرسمي.

الخلفية

في أيلول (سبتمبر) 2016، أضافت Safari الدعم لميزة "نافذة ضمن النافذة" من خلال واجهة برمجة تطبيقات WebKit API. في نظام التشغيل macOS Sierra بعد مرور ستة أشهر، شغّل Chrome تلقائيًا فيديو "نافذة ضمن النافذة" على الأجهزة الجوّالة مع إصدار Android O باستخدام واجهة برمجة تطبيقات Android الأصلية. بعد مرور ستة أشهر، أعلنّا عن إنشاء لتوحيد واجهة برمجة تطبيقات الويب، وهي ميزة متوافقة مع متصفح 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;
  }
});

ويعمل عنصر الفيديو بالطريقة نفسها سواء كان في نافذة ضمن النافذة أو not: يتم تنشيط الأحداث وتعمل طرق الاتصال. إنه يعكس تغييرات الحالة في نافذة "نافذة ضمن النافذة" (مثل التشغيل أو الإيقاف المؤقت أو التقديم أو غير ذلك) كما هي من الممكن تغيير الحالة آليًا في 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.
});

تخصيص نافذة "نافذة ضمن النافذة"

يوفّر Chrome 74 أزرار التشغيل/الإيقاف المؤقت، والمقطع الصوتي السابق، والمقطع الصوتي التالي في يمكنك التحكّم في نافذة ميزة "نافذة ضمن النافذة" باستخدام واجهة برمجة التطبيقات لجلسة الوسائط.

عناصر التحكم في تشغيل الوسائط في نافذة &quot;نافذة ضمن النافذة&quot;
الشكل 1. عناصر التحكم في تشغيل الوسائط في نافذة "نافذة ضمن النافذة"

يظهر زر التشغيل أو الإيقاف المؤقت بشكل تلقائي في نافذة ضمن النافذة. ما لم يشغّل الفيديو عناصر MediaStream (مثل getUserMedia()، getDisplayMedia() أو canvas.captureStream()) أو أنّ الفيديو يتضمّن MediaSource. تم ضبط المدة على +Infinity (مثل خلاصة بث مباشر). للتأكّد من اختيار زر التشغيل أو الإيقاف المؤقت مرئي دائمًا، اضبط بعض معالجات إجراءات جلسة الوسائط لكل من "تشغيل" أو إيقاف مؤقت ("Pause") الوسائط المختلفة كما هو موضح أدناه.

// 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.
}

أقترح عدم الانجذاب إلى حدث تغيير الحجم مباشرةً، إذ يتم إجراء كل تغيير صغير على حجم نافذة "نافذة ضمن النافذة" يؤدي إلى تنشيط حدث منفصل قد يؤدي إلى ومشكلات الأداء إذا كنت تجري عملية مكلفة عند كل تغيير حجم. ضِمن بمعنى آخر، ستؤدي عملية تغيير الحجم إلى تنشيط الأحداث مرارًا وتكرارًا بسرعة. أنصح باستخدام الأساليب الشائعة مثل التقييد بشكل متكرر لحل هذه المشكلة.

إتاحة الميزات

قد تكون واجهة برمجة تطبيقات الويب لميزة "نافذة ضمن النافذة" غير متوافقة، لذا يجب رصد هذه الميزة. لتوفير التحسين التدريجي. حتى عندما يكون معتمدًا، فقد يكون تم إيقافها من قِبل المستخدم أو تم إيقافها بواسطة سياسة الأذونات. لحسن الحظ، يمكنك استخدام باستخدام القيمة المنطقية الجديدة 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() مع واجهة برمجة التطبيقات لجلسات الوسائط، يمكنك إجراء ما يلي: مثلاً إنشاء نافذة قائمة تشغيل صوتية في Chrome 74. اطّلِع على المزايا الرسمية نموذج لقائمة تشغيل صوتية:

قائمة تشغيل الصوت في نافذة &quot;نافذة ضمن النافذة&quot;
الشكل 2. قائمة تشغيل الصوت في نافذة "نافذة ضمن النافذة"

العيّنات والعروض التوضيحية ودروس الترميز

يمكنك الاطّلاع على نموذج "نافذة ضمن النافذة" الرسمي لتجربة الميزة "نافذة ضمن النافذة". واجهة برمجة تطبيقات الويب.

وبعدها، سيسري البرنامج على العروض التوضيحية والدروس التطبيقية حول الترميز.

الخطوات التالية

أولاً، يمكنك الاطلاع على صفحة حالة التنفيذ لمعرفة أجزاء يتم تنفيذ واجهة برمجة التطبيقات حاليًا في Chrome والمتصفحات الأخرى.

وفي ما يلي ما يمكن أن تتوقع مشاهدته في المستقبل القريب:

دعم المتصفح

تتوفّر واجهة برمجة تطبيقات الويب لميزة "نافذة ضمن النافذة" في Chrome وEdge وOpera وSafari. راجع MDN لمعرفة التفاصيل.

الموارد

نتوجّه بالشكر إلى "منير لاموري" و"جنيفر أباسيبل" على عملهما. ميزة "نافذة ضمن النافذة"، بالإضافة إلى المساعدة في هذه المقالة وشكرًا جزيلاً للجميع المشاركة في جهود توحيد المعايير.