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

François Beaufort
François Beaufort

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

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

רקע

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

כניסה לקוד

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

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

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

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

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

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

פקדים להפעלת מדיה בחלון &#39;תמונה בתוך תמונה&#39;
איור 1. פקדים להפעלת מדיה בחלון 'תמונה בתוך תמונה'

כברירת מחדל, לחצן הפעלה/השהיה תמיד מוצג בחלון 'תמונה בתוך תמונה', אלא אם הסרטון מפעיל אובייקטים של MediaStream (למשל getUserMedia(), getDisplayMedia(), canvas.captureStream()) או שמשך הזמן של MediaSource בסרטון מוגדר ל-+Infinity (למשל, פיד פעיל). כדי להבטיח שלחצן הפעלה/השהיה יהיה גלוי תמיד, מגדירים רכיבי handler של פעולות בסשן מדיה, גם לאירועי מדיה 'הפעלה' וגם ל'השהיה', כפי שמתואר בהמשך.

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

פקדי החלון 'הטראק הקודם' ו'הטראק הבא' דומים. אם מגדירים רכיבי 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) והסרת נתונים.

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

יכול להיות שאין תמיכה ב-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() עם MediaSession API מאפשר ליצור חלון של פלייליסט של אודיו ב-Chrome 74. נסו את הקטע הרשמי לדוגמה של פלייליסט של אודיו.

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

טעימות, הדגמות ו-codelabs

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

יהיו הדגמות ו-Codelabs בהמשך.

מה השלב הבא?

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

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

תמיכת דפדפן

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

משאבים

תודה רבה למוניר לאמורי וג'ניפר אפאסבל על העבודה שלהם על 'תמונה בתוך תמונה' ועל העזרה במאמר הזה. ותודה ענקית לכל מי שלקח חלק במאמץ לתקינה.