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

François Beaufort
François Beaufort

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

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

רקע

בספטמבר 2016, הוספנו ל-Safari תמיכה ב'תמונה בתוך תמונה' באמצעות WebKit API ב-macOS Sierra. שישה חודשים לאחר מכן, עם השקת Android O, Chrome התחיל להפעיל סרטונים במצב 'תמונה בתוך תמונה' באופן אוטומטי בניידים באמצעות Android API מקורי. שישה חודשים לאחר מכן, הודענו על הכוונה שלנו ליצור ולקבוע תקן לממשק 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(). אחרי זה הסרטון יופיע שוב בכרטיסייה המקורית. שימו לב שהשיטה הזו גם מחזירה הבטחה (promise).

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

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

כדי לראות איך זה עובד, אפשר לנסות את הדוגמה הרשמית של Media Session.

אחזור גודל החלון של 'תמונה בתוך תמונה'

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

בדוגמה הבאה מוסבר איך לקבל את הרוחב והגובה של חלון 'תמונה בתוך תמונה' כשהוא נוצר או כשמשנים את הגודל שלו.

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

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

פלייליסט אודיו בחלון של &#39;תמונה בתוך תמונה&#39;
איור 2. פלייליסט אודיו בחלון של 'תמונה בתוך תמונה'

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

כדאי לעיין בדוגמה הרשמית לתמונה בתוך תמונה כדי לנסות את Picture-in-Picture Web API.

בהמשך יפורסמו הדגמות והדרכות בנושא.

מה השלב הבא?

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

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

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

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

משאבים

תודה רבה למוניר למורי (Mounir Lamouri) ולג'ניפר אפסיליבל (Jennifer Apacible) על העבודה שלהם על התכונה 'תמונה בתוך תמונה' ועל העזרה במאמר הזה. תודה רבה לכל מי שהיה מעורב במאמץ התקינה.