צילום אודיו ווידאו ב-HTML5

מבוא

צילום אודיו/וידאו הוא ה "גביע הקדוש" של פיתוח אתרים כבר הרבה זמן. במשך שנים רבות היינו צריכים להסתמך על יישומי פלאגין לדפדפן (Flash או Silverlight) כדי לבצע את העבודה. קדימה!

HTML5 בא לעזרתנו. אולי זה לא נראה ברור, אבל העלייה בשימוש ב-HTML5 הביאה לעלייה בגישה לחומרת המכשיר. מיקום גיאוגרפי (GPS), Orientation API (מד תאוצה), WebGL (GPU), ו-Web Audio API (חומרת אודיו) הם דוגמאות מצוינות. התכונות האלה חזקות מאוד, וחושפות ממשקי API של JavaScript ברמה גבוהה שמתבססים על יכולות החומרה הבסיסיות של המערכת.

במדריך הזה נציג ממשק API חדש, GetUserMedia, שמאפשר לאפליקציות אינטרנט לגשת למצלמה ולמיקרופון של המשתמש.

הדרך אל getUserMedia()‎

אם אתם לא מכירים את ההיסטוריה של ה-API של getUserMedia(), הדרך שבה הגענו אליו היא סיפור מעניין.

בשנים האחרונות פותחו כמה גרסאות של ממשקי API לצילום מדיה. הרבה אנשים זיהו את הצורך בגישה למכשירים מקוריים באינטרנט, אבל זה הוביל לכך שכולם יצרו מפרט חדש. המצב נהיה כל כך מבולגן, עד ש-W3C החליטו בסופו של דבר להקים קבוצת עבודה. המטרה היחידה שלהם? עושים סדר בבלגן! קבוצת העבודה של Device APIs Policy (DAP)‎ קיבלה את המשימה לאחד את ההצעות הרבות וליצור מהן תקן.

אנסה לסכם את מה שקרה ב-2011…

סבב 1: HTML Media Capture

HTML Media Capture היה הניסיון הראשון של DAP ליצור סטנדרטיזציה של צילום מדיה באינטרנט. הוא פועל על ידי עומס יתר של <input type="file"> והוספת ערכים חדשים לפרמטר accept.

אם אתם רוצים לאפשר למשתמשים לצלם את עצמם באמצעות מצלמת האינטרנט, אתם יכולים לעשות את זה באמצעות capture=camera:

<input type="file" accept="image/*;capture=camera">

הקלטת סרטון או אודיו דומה:

<input type="file" accept="video/*;capture=camcorder">
<input type="file" accept="audio/*;capture=microphone">

נחמד, נכון? אני אוהב במיוחד את העובדה שהוא משתמש מחדש בקלט של קובץ. מבחינה סמנטית, זה הגיוני מאוד. החיסרון של ה-API הספציפי הזה הוא היכולת לבצע אפקטים בזמן אמת (למשל, עיבוד נתונים של מצלמת אינטרנט בזמן אמת ל-<canvas> והחלת מסנני WebGL). HTML Media Capture מאפשר רק להקליט קובץ מדיה או לצלם תמונה ברגע נתון.

תמיכה:

  • דפדפן Android 3.0 – אחת מההטמעות הראשונות. בסרטון הזה אפשר לראות איך זה עובד.
  • ‫Chrome ל-Android‏ (0.16)
  • Firefox Mobile 10.0
  • ‫iOS6 Safari ו-Chrome (תמיכה חלקית)

סבב 2: רכיב המכשיר

הרבה אנשים חשבו ש-HTML Media Capture מגביל מדי, ולכן נוצר מפרט חדש שתומך בכל סוג של מכשיר (עתידי). באופן לא מפתיע, העיצוב דרש רכיב חדש, הרכיב <device>, שהפך לקודמו של getUserMedia().

דפדפן Opera היה בין הראשונים שיצרו הטמעות ראשוניות של צילום וידאו על בסיס הרכיב <device>. זמן קצר לאחר מכן (באותו יום, ליתר דיוק), WhatWG החליטו לבטל את התג <device> לטובת תג אחר, והפעם API של JavaScript שנקרא navigator.getUserMedia(). שבוע לאחר מכן, Opera הוציאה גרסאות חדשות שכללו תמיכה במפרט המעודכן של getUserMedia(). בהמשך אותה שנה, מיקרוסופט הצטרפה לחגיגה והוציאה את Lab for IE9 שתומך במפרט החדש.

ככה <device> הייתה נראית:

<device type="media" onchange="update(this.data)"></device>
<video autoplay></video>
<script>
  function update(stream) {
    document.querySelector('video').src = stream.url;
  }
</script>

תמיכה:

לצערנו, אף דפדפן שפורסם לא כלל את <device>. אני מניח שזה API אחד פחות שצריך לדאוג לגביו :) עם זאת, ל-<device> היו שני דברים נהדרים: 1.) הוא היה סמנטי, ו-2.) היה קל להרחיב אותו כדי לתמוך ביותר מסתם מכשירי אודיו/וידאו.

קח רגע. הדברים האלה זזים מהר!

סבב 3: WebRTC

בסופו של דבר, הרכיב <device> נעלם מהעולם.

הקצב למציאת API מתאים לצילום מסך הואץ בזכות המאמץ הגדול יותר של WebRTC (תקשורת בזמן אמת באינטרנט). המפרט הזה נמצא בפיקוח של קבוצת העבודה של W3C WebRTC. ל-Google, ל-Opera, ל-Mozilla ולעוד כמה חברות יש הטמעות.

השדה getUserMedia() קשור ל-WebRTC כי הוא השער לקבוצת ממשקי ה-API הזו. הוא מספק את האמצעים לגשת לשידור המקומי של המצלמה או המיקרופון של המשתמש.

תמיכה:

getUserMedia() נתמך החל מגרסה 21 של Chrome, גרסה 18 של Opera וגרסה 17 של Firefox.

תחילת העבודה

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

זיהוי תכונות

זיהוי תכונות הוא בדיקה פשוטה של קיום navigator.mediaDevices.getUserMedia:

if (navigator.mediaDevices?.getUserMedia) {
  // Good to go!
} else {
  alert("navigator.mediaDevices.getUserMedia() is not supported");
}

קבלת גישה למכשיר לקליטת נתונים

כדי להשתמש במצלמת האינטרנט או במיקרופון, אנחנו צריכים לבקש הרשאה. הפרמטר הראשון של navigator.mediaDevices.getUserMedia() הוא אובייקט שמציין את הפרטים והדרישות של כל סוג מדיה שרוצים לגשת אליו. לדוגמה, אם רוצים לגשת למצלמת האינטרנט, הפרמטר הראשון צריך להיות {video: true}. כדי להשתמש גם במיקרופון וגם במצלמה, צריך להעביר את הערך {video: true, audio: true}:

<video autoplay></video>

<script>
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: true })
    .then((localMediaStream) => {
      const video = document.querySelector("video");
      video.srcObject = localMediaStream;
    })
    .catch((error) => {
      console.log("Rejected!", error);
    });
</script>

אוקיי. אז מה קורה כאן? צילום מדיה הוא דוגמה מושלמת לממשקי API חדשים של HTML5 שפועלים יחד. הוא פועל בשילוב עם רכיבי HTML5 האחרים שלנו, <audio> ו-<video>. שימו לב שלא הגדרנו מאפיין src ולא כללנו רכיבי <source> ברכיב <video>. במקום להזין לסרטון כתובת URL של קובץ מדיה, אנחנו מגדירים את srcObject לאובייקט LocalMediaStream שמייצג את מצלמת האינטרנט.

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

הגדרת מגבלות מדיה (רזולוציה, גובה, רוחב)

אפשר להשתמש בפרמטר הראשון של getUserMedia() גם כדי לציין דרישות (או אילוצים) נוספות לגבי זרם המדיה שמוחזר. לדוגמה, במקום לציין שאתם רוצים גישה בסיסית לווידאו (למשל {video: true}), אתם יכולים לדרוש שהשידור יהיה באיכות HD:

const hdConstraints = {
  video: { width: { exact:  1280} , height: { exact: 720 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);
const vgaConstraints = {
  video: { width: { exact:  640} , height: { exact: 360 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);

להגדרות נוספות, אפשר לעיין בממשק ה-API של האילוצים.

בחירת מקור מדיה

השיטה enumerateDevices() של הממשק MediaDevices מבקשת רשימה של מכשירי הקלט ומכשירי להצגת מידע הזמינים של המדיה, כמו מיקרופונים, מצלמות, אוזניות וכו'. ה-Promise שמוחזר נפתר באמצעות מערך של אובייקטים מסוג MediaDeviceInfo שמתארים את המכשירים.

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

if (!navigator.mediaDevices?.enumerateDevices) {
  console.log("enumerateDevices() not supported.");
} else {
  // List cameras and microphones.
  navigator.mediaDevices
    .enumerateDevices()
    .then((devices) => {
      let audioSource = null;
      let videoSource = null;

      devices.forEach((device) => {
        if (device.kind === "audioinput") {
          audioSource = device.deviceId;
        } else if (device.kind === "videoinput") {
          videoSource = device.deviceId;
        }
      });
      sourceSelected(audioSource, videoSource);
    })
    .catch((err) => {
      console.error(`${err.name}: ${err.message}`);
    });
}

async function sourceSelected(audioSource, videoSource) {
  const constraints = {
    audio: { deviceId: audioSource },
    video: { deviceId: videoSource },
  };
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
}

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

אבטחה

בדפדפנים מוצגת תיבת דו-שיח של הרשאות כשמתקשרים אל navigator.mediaDevices.getUserMedia(), שמאפשרת למשתמשים לתת או לדחות גישה למצלמה או למיקרופון שלהם. לדוגמה, כך נראית תיבת הדו-שיח של ההרשאות ב-Chrome:

תיבת דו-שיח של הרשאות ב-Chrome
תיבת דו-שיח של הרשאות ב-Chrome

מתן חלופה

למשתמשים שאין להם תמיכה ב-navigator.mediaDevices.getUserMedia(), אפשרות אחת היא לחזור לקובץ וידאו קיים אם ה-API לא נתמך או אם הקריאה נכשלת מסיבה כלשהי:

if (!navigator.mediaDevices?.getUserMedia) {
  video.src = "fallbackvideo.webm";
} else {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  video.srcObject = stream;
}