גלילה ושינוי מרחק התצוגה של כרטיסייה שצולמה

François Beaufort
François Beaufort

כבר אפשר לשתף כרטיסיות, חלונות ומסכים בפלטפורמת האינטרנט באמצעות Screen Capture API. כשאפליקציית אינטרנט קוראת ל-getDisplayMedia(), Chrome מציג למשתמש בקשה לשתף כרטיסייה, חלון או מסך עם אפליקציית האינטרנט כסרטון MediaStreamTrack.

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

במסמך הזה נסביר על Captured Surface Control API החדש ב-Chrome, שמאפשר לאפליקציית האינטרנט לגלול בכרטיסייה שצולמה, וגם לקרוא ולכתוב את רמת הזום של כרטיסייה שצולמה.

משתמש גולל בכרטיסייה שצולמה ומגדיל את התצוגה שלה (הדגמה).

למה כדאי להשתמש ב-Captured Surface Control?

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

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

ממשק ה-API לניהול משטחים שתועדו פותר את הבעיות האלה.

איך משתמשים ב'שליטה בשטח התצוגה שצולם'?

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

איך מצלמים כרטיסייה בדפדפן

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

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({ controller });

בשלב הבא, יוצרים תצוגה מקדימה מקומית של המשטח שצולם בצורת רכיב <video>:

const previewTile = document.querySelector('video');
previewTile.srcObject = stream;

אם המשתמש יבחר לשתף חלון או מסך, זה לא יהיה בטווח האפשרויות שלנו כרגע – אבל אם הוא יבחר לשתף כרטיסייה, נוכל להמשיך.

const [track] = stream.getVideoTracks();

if (track.getSettings().displaySurface !== 'browser') {
  // Bail out early if the user didn't pick a tab.
  return;
}

בקשת הרשאה

ההפעלה הראשונה של sendWheel() או setZoomLevel() באובייקט CaptureController נתון מובילה להצגת בקשה להרשאה. אם המשתמש מעניק הרשאה, קריאות נוספות לשיטות האלה באובייקט CaptureController הזה מותרות. אם המשתמש דוחה את ההרשאה, ההבטחה שהוחזרה נדחית.

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

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

const startScrollingButton = document.querySelector('button');

startScrollingButton.addEventListener('click', async () => {
  try {
    const noOpWheelAction = {};

    await controller.sendWheel(noOpWheelAction);
    // The user approved the permission prompt.
    // You can now scroll and zoom the captured tab as shown later in the article.
  } catch (error) {
    return; // Permission denied. Bail.
  }
});

גלילה

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

בהנחה שאפליקציית הצילום משתמשת ברכיב <video> שנקרא "previewTile", הקוד הבא מראה איך להעביר אירועי שליחת גלגל לכרטיסייה שצולמה:

const previewTile = document.querySelector('video');

previewTile.addEventListener('wheel', async (event) => {
  // Translate the offsets into coordinates which sendWheel() can understand.
  // The implementation of this translation is explained further below.
  const [x, y] = translateCoordinates(event.offsetX, event.offsetY);
  const [wheelDeltaX, wheelDeltaY] = [-event.deltaX, -event.deltaY];

  try {
    // Relay the user's action to the captured tab.
    await controller.sendWheel({ x, y, wheelDeltaX, wheelDeltaY });
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

השיטה sendWheel() מקבלת מילון עם שתי קבוצות של ערכים:

  • x ו-y: הקואורדינטות שבהן צריך לשלוח את אירוע הגלילה.
  • wheelDeltaX ו-wheelDeltaY: עוצמת הגלילה, בפיקסלים, לגלילה אופקית ואנכית, בהתאמה. הערכים האלה הפוכים לעומת הערכים באירוע המקורי של גלגל העכבר.

הטמעה אפשרית של translateCoordinates() היא:

function translateCoordinates(offsetX, offsetY) {
  const previewDimensions = previewTile.getBoundingClientRect();
  const trackSettings = previewTile.srcObject.getVideoTracks()[0].getSettings();

  const x = trackSettings.width * offsetX / previewDimensions.width;
  const y = trackSettings.height * offsetY / previewDimensions.height;

  return [Math.floor(x), Math.floor(y)];
}

חשוב לשים לב שיש שלוש מידות שונות בקוד שלמעלה:

  • הגודל של רכיב <video>.
  • הגודל של הפריימים שצולמו (מיוצגים כאן כ-trackSettings.width ו-trackSettings.height).
  • גודל הכרטיסייה.

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

אפליקציית האינטרנט משתמשת ב-translateCoordinates() כדי לתרגם את ההיסטים ביחס לרכיב <video> לקואורדינטות במרחב הקואורדינטות של טראק הסרטון. הדפדפן יתרגם גם בין גודל הפריים שצולם לבין גודל הכרטיסייה, וישלח את אירוע הגלילה בהיסט שתואם לציפייה של אפליקציית האינטרנט.

ההבטחה שמוחזרת על ידי sendWheel() יכולה להידחות במקרים הבאים:

  • אם סשן הצילום עדיין לא התחיל או שכבר הופסק, כולל עצירה אסינכררונית בזמן שהדפדפן מטפל בפעולה sendWheel().
  • אם המשתמש לא העניק לאפליקציה הרשאה להשתמש ב-sendWheel().
  • אם אפליקציית הצילום מנסה לשלוח אירוע גלילה בקואורדינטות שמחוץ ל-[trackSettings.width, trackSettings.height]. חשוב לזכור שהערכים האלה עשויים להשתנות באופן אסינכרוני, לכן מומלץ לזהות את השגיאה ולהתעלם ממנה. (הערה: בדרך כלל, 0, 0 לא יהיה מחוץ לתחום, כך שאפשר להשתמש בו כדי לבקש מהמשתמש הרשאה).

שינוי מרחק התצוגה

אפשר לבצע פעולות ברמת הזום של הכרטיסייה שצולמה באמצעות הממשקים הבאים של CaptureController:

  • הפונקציה getSupportedZoomLevels() מחזירה רשימה של רמות זום שנתמכות בדפדפן, שמוצגות כאחוזים מ'רמת הזום שמוגדרת כברירת מחדל', שמוגדרת כ-100%. הרשימה הזו עולה באופן מונוטוני ומכילה את הערך 100.
  • הפונקציה getZoomLevel() מחזירה את רמת הזום הנוכחית של הכרטיסייה.
  • setZoomLevel() מגדיר את רמת הזום של הכרטיסייה לכל ערך שלם שנמצא ב-getSupportedZoomLevels(), ומחזיר הבטחה (promise) כשהפעולה מסתיימת בהצלחה. חשוב לדעת שרמת הזום לא מתאפסת בסוף סשן הצילום.
  • oncapturedzoomlevelchange מאפשר לכם להאזין לשינויים ברמת הזום של כרטיסייה שצולמה, כי המשתמשים יכולים לשנות את רמת הזום דרך אפליקציית הצילום או דרך אינטראקציה ישירה עם הכרטיסייה שצולמה.

קריאות ל-setZoomLevel() מוגבלות על ידי הרשאה. קריאות לשיטות אחרות של zoom לקריאה בלבד הן 'חינמיות', וכך גם האזנה לאירועים.

בדוגמה הבאה מוסבר איך להגדיל את רמת הזום של כרטיסייה שצולמה בסשן צילום קיים:

const zoomIncreaseButton = document.getElementById('zoomInButton');

zoomIncreaseButton.addEventListener('click', async (event) => {
  const levels = CaptureController.getSupportedZoomLevels();
  const index = levels.indexOf(controller.getZoomLevel());
  const newZoomLevel = levels[Math.min(index + 1, levels.length - 1)];

  try {
    await controller.setZoomLevel(newZoomLevel);
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

בדוגמה הבאה מוסבר איך להגיב לשינויים ברמת הזום של כרטיסייה שצולמה:

controller.addEventListener('capturedzoomlevelchange', (event) => {
  const zoomLevel = controller.getZoomLevel();
  document.querySelector('#zoomLevelLabel').textContent = `${zoomLevel}%`;
});

זיהוי תכונות

כדי לבדוק אם יש תמיכה בשליחת אירועי גלגל, משתמשים ב-:

if (!!window.CaptureController?.prototype.sendWheel) {
  // CaptureController sendWheel() is supported.
}

כדי לבדוק אם יש תמיכה בשליטה על הזום, משתמשים באפשרויות הבאות:

if (!!window.CaptureController?.prototype.setZoomLevel) {
  // CaptureController setZoomLevel() is supported.
}

הפעלת בקרת משטח צילום

ממשק ה-API לניהול משטח נתפס זמין ב-Chrome למחשבים נייחים מאחורי הדגל Captured Surface Control, וניתן להפעיל אותו בכתובת chrome://flags/#captured-surface-control.

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

אבטחה ופרטיות

מדיניות ההרשאות של "captured-surface-control" מאפשרת לכם לקבוע לאילו אפליקציות הקלטה ולפריטי iframe מוטמעים של צד שלישי תהיה גישה ל-Captured Surface Control. כדי להבין את הפשרות באבטחה, כדאי לעיין בקטע שיקולים לגבי פרטיות ואבטחה במאמר ההסבר על אמצעי הבקרה של משטח הנתונים המצולם.

הדגמה (דמו)

כדי לנסות את התכונה 'שליטה במשטח שצולם', אפשר להריץ את הדמו ב-Glitch. חשוב לבדוק את קוד המקור.

שינויים מגרסאות קודמות של Chrome

ריכזנו כאן כמה הבדלים התנהגותיים עיקריים לגבי אמצעי הבקרה של משטח מוצג שחשוב להכיר:

  • ב-Chrome 124 ובגרסאות קודמות:
    • ההרשאה – אם היא ניתנת – מוגבלת לסשן הצילום שמשויך ל-CaptureController הזה, ולא למקור הצילום.
  • ב-Chrome 122:
    • הפונקציה getZoomLevel() מחזירה הבטחה עם רמת הזום הנוכחית של הכרטיסייה.
    • אם המשתמש לא העניק לאפליקציה הרשאת שימוש, הפונקציה sendWheel() מחזירה הבטחה שנדחתה עם הודעת השגיאה "No permission.". סוג השגיאה הוא "NotAllowedError" ב-Chrome 123 ואילך.
    • המקור שנבחר (oncapturedzoomlevelchange) לא זמין. אפשר להשתמש ב-polyfill של התכונה הזו באמצעות setInterval().

משוב

צוות Chrome וקהילת תקני האינטרנט רוצים לשמוע על החוויות שלכם עם Captured Surface Control.

נשמח לשמוע על העיצוב

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

בעיה בהטמעה?

מצאתם באג בהטמעה של Chrome? או שההטמעה שונה מהמפרט? שולחים דיווח על באג בכתובת https://new.crbug.com. חשוב לכלול כמה שיותר פרטים, וגם הוראות לשחזור הבעיה. Glitch הוא כלי מצוין לשיתוף באגים שניתן לשחזור.