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

François Beaufort
François Beaufort

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

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

الخلفية

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

يوضح قسم دعم الميزات أدناه كيفية تفعيل/إيقاف زر استنادًا إلى هذه القيود.

ويجب إضافة كتلة 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;
  }
});

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

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

يظهر دائمًا زر التشغيل/الإيقاف المؤقت بشكل تلقائي في نافذة ميزة "نافذة ضمن النافذة" إلا إذا كان الفيديو يشغّل كائنات MediaStream (مثل getUserMedia() أو getDisplayMedia() أو canvas.captureStream()) أو إذا تم ضبط مدة MediaSource على +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.
}

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

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

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

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

النماذج التجريبية والعروض التوضيحية والدروس التطبيقية حول الترميز

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

يتبع ذلك العروض التوضيحية والدروس التطبيقية حول الترميز.

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

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

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

المتصفحات المتوافقة

يمكن استخدام واجهة برمجة تطبيقات الويب نافذة ضمن النافذة في Chrome وEdge وOpera وSafari. لمزيد من التفاصيل، يُرجى الاطّلاع على MDN.

المراجِع

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