צפייה בסרטון במצב 'תמונה בתוך תמונה'

François Beaufort
François Beaufort

התכונה 'תמונה בתוך תמונה' (PiP) מאפשרת למשתמשים לצפות בסרטונים בחלון צף (תמיד מעל חלונות אחרים) כדי שהם יוכלו להשגיח על מה שהם בזמן שאתם צופים במהלך אינטראקציה עם אתרים או אפליקציות אחרים.

באמצעות Picture-in-Picture Web API אתם יכולים להפעיל ולשלוט רכיבי 'תמונה בתוך תמונה' לסרטונים באתר שלכם. כדאי לנסות דוגמה רשמית של תמונה בתוך תמונה.

רקע

בספטמבר 2016, הוסיפה ב-Safari תמיכה ב'תמונה בתוך תמונה' באמצעות WebKit API ב-macOS Sierra. שישה חודשים לאחר מכן, Chrome הופעל באופן אוטומטי סרטון מסוג 'תמונה בתוך תמונה' בנייד שהושק ב-Android O באמצעות API מקורי של Android. שישה חודשים לאחר מכן הכרזנו על הכוונה שלנו ליצור ליצור סטנדרטיזציה של ממשק API לאינטרנט, תכונה שתואמת ל-Safari, שיאפשר למפתחים ליצור ולשלוט בכל החוויה סביב 'תמונה בתוך תמונה'. ואנחנו כאן!

כניסה לקוד

מעבר למצב 'תמונה בתוך תמונה'

נתחיל עם רכיב וידאו ודרך שבה המשתמשים יוכלו לקיים אינטראקציה איתו, למשל אלמנט של לחצן.

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

לבקש מצב 'תמונה בתוך תמונה' רק בתגובה לתנועת משתמש, ואף פעם לא promise הוחזרה על ידי videoElement.play(). כי הבטחות לא עדיין מפיצים תנועות משתמשים. במקום זאת, התקשרו אל requestPictureInPicture() ה-handler של הקליקים על pipButtonElement כמו שמוצג בהמשך. באחריותך טיפול במה שקורה אם משתמש לוחץ פעמיים.

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

כשההבטחה נפסקת, Chrome מכווץ את הסרטון לחלון קטן שהמשתמש יכול לעבור ממקום למקום ולמקם אותו מעל חלונות אחרים.

סיימתם. מעולה! אפשר להפסיק לקרוא וללכת לקחת 'חופשה'. לצערי, זה לא תמיד נכון. ההבטחה עשויה לדחות כל מהסיבות הבאות:

  • המערכת לא תומכת ב'תמונה בתוך תמונה'.
  • אין במסמך אישור להשתמש ב'תמונה בתוך תמונה' עקב מגבלות מדיניות ההרשאות.
  • המטא-נתונים של הסרטון עדיין לא נטענו (videoElement.readyState === 0).
  • קובץ הווידאו כולל אודיו בלבד.
  • המאפיין החדש disablePictureInPicture נמצא ברכיב הווידאו.
  • השיחה לא בוצעה ברכיב handler של אירועים מסוג תנועה של משתמש (למשל, לחיצה על לחצן). החל מגרסה 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.
});

התאמה אישית של חלון עם תמונה בתוך תמונה

ב-Chrome 74 יש תמיכה בלחצנים להפעלה/השהיה, לטראק הקודם ולטראק הבא חלון מסוג 'תמונה בתוך תמונה' שאפשר לשלוט בו באמצעות Media Session API.

פקדי הפעלת מדיה בחלון &#39;תמונה בתוך תמונה&#39;
איור 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.
});

מציג את "הטראק הקודם" ו-'Next track' (הטראק הבא) פקדי החלונות דומים. ההגדרה רכיבי handler של פעולות בסשן מדיה עבור האנשים האלה יציגו אותם במצב 'תמונה בתוך תמונה' כך תוכלו לבצע את הפעולות האלה.

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

מומלץ לא להתחבר ישירות לאירוע שינוי הגודל, כי כל שינוי קטן לגודל של חלון 'תמונה בתוך תמונה' יופעל אירוע נפרד שעלול לגרום אם אתם מבצעים פעולה יקרה בכל שינוי גודל. לחשבון במילים אחרות, פעולת שינוי הגודל תפעיל את האירועים שוב ושוב במהירות. אני ממליץ להשתמש בשיטות נפוצות כמו ויסות נתונים (throttle) debouncing כדי לטפל בבעיה הזו.

תמיכה בתכונות

יכול להיות ש-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()) יש תמיכה גם ב'תמונה בתוך תמונה' ב-Chrome 71. הזה המשמעות היא שאפשר להציג חלון עם תמונה בתוך תמונה שמכיל את מצלמת האינטרנט של המשתמש. וידאו בסטרימינג, הצגת וידאו בסטרימינג או אפילו רכיב של קנבס. שימו לב רכיב הווידאו לא חייב להיות מצורף ל-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. לצפייה בדף הרשמי דוגמה לפלייליסט של אודיו.

פלייליסט של אודיו בחלון עם תמונה בתוך תמונה
איור 2. פלייליסט של אודיו בחלון עם תמונה בתוך תמונה

דגימות, הדגמות ו-Codelabs

כדאי לצפות בדוגמה הרשמית של תמונה בתוך תמונה כדי לנסות את התכונה 'תמונה בתוך תמונה' ממשק API לאינטרנט.

הדגמות ו-Codelabs פועלים.

מה השלב הבא?

קודם כול, אפשר לעיין בדף של סטטוס ההטמעה כדי לראות אילו חלקים ממשקי API מוטמעים כרגע ב-Chrome ובדפדפנים אחרים.

הנה מה שצפוי לראות בעתיד הקרוב:

תמיכה בדפדפנים

ה-API של 'תמונה בתוך תמונה' נתמך ב-Chrome, ב-Edge, ב-Opera וב-Safari. פרטים נוספים זמינים במאמר MDN.

משאבים

תודה רבה למוניר למורי וג'ניפר אפקטיב (Jennifer Apacible) על עבודתם 'תמונה בתוך תמונה', והעזרה במאמר הזה. ותודה רבה לכולם חלק ממאמץ הסטנדרטיזציה.