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

François Beaufort
François Beaufort

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

باستخدام 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;
  }
});

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

في ما يلي الميزات التي يمكنك توقّعها في المستقبل القريب:

دعم المتصفح

تتوفّر واجهة برمجة التطبيقات Picture-in-Picture Web API في متصفّحات Chrome وEdge وOpera وSafari. راجِع MDN للحصول على التفاصيل.

الموارد

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