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

François Beaufort
François Beaufort

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

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

الخلفية

في أيلول (سبتمبر) 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;
  }
});

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

عناصر التحكّم في تشغيل الوسائط في نافذة &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.
}

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

إتاحة الميزة

قد لا تكون واجهة برمجة التطبيقات 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. يمكنك الاطّلاع على نموذج قائمة تشغيل صوتية الرسمي.

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

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

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

وستتوفّر العروض التوضيحية والدروس التطبيقية حول الترميز لاحقًا.

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

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

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

دعم المتصفح

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

الموارد

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