צילום תמונה מהמשתמש

ברוב הדפדפנים יש גישה למצלמה של המשתמש.

קנה מידה של משטח

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

התחלה פשוטה ובהדרגה

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

בקשת כתובת אתר

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

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

קלט הקובץ

אפשר גם להשתמש ברכיב פשוט של קלט קובץ, כולל מסנן accept שמציין שאתם רוצים רק קובצי תמונה.

<input type="file" accept="image/*" />

השיטה הזו פועלת בכל הפלטפורמות. במחשב השולחני, המשתמש יתבקש להעלות קובץ תמונה ממערכת הקבצים. ב-Chrome וב-Safari ב-iOS וב-Android, השיטה הזו תאפשר למשתמש לבחור באיזו אפליקציה להשתמש לצילום התמונה, כולל אפשרות לצלם תמונה ישירות באמצעות המצלמה או לבחור קובץ תמונה קיים.

תפריט Android, עם שתי אפשרויות: צילום תמונה וקבצים תפריט iOS, עם שלוש אפשרויות: צילום תמונה, ספריית תמונות ו-iCloud

לאחר מכן אפשר לצרף את הנתונים ל-<form> או לשנות אותם באמצעות JavaScript. לשם כך, מאזינים לאירוע onchange ברכיב הקלט וקוראים את המאפיין files של האירוע target.

<input type="file" accept="image/*" id="file-input" />
<script>
  const fileInput = document.getElementById('file-input');

  fileInput.addEventListener('change', (e) =>
    doSomethingWithFiles(e.target.files),
  );
</script>

המאפיין files הוא אובייקט FileList, שעליו ארחיב בהמשך.

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

<input type="file" accept="image/*" capture />
<input type="file" accept="image/*" capture="user" />
<input type="file" accept="image/*" capture="environment" />

אם מוסיפים את המאפיין capture ללא ערך, הדפדפן יחליט באיזו מצלמה להשתמש. הערכים "user" ו-"environment" מורים לדפדפן להעדיף את המצלמה הקדמית והאחורית, בהתאמה.

המאפיין capture פועל ב-Android וב-iOS, אבל המערכת מתעלמת ממנו במחשב. עם זאת, שימו לב שב-Android המשמעות היא שלמשתמש לא תהיה יותר אפשרות לבחור תמונה קיימת. אפליקציית מצלמת המערכת תופעל ישירות במקום זאת.

גרירה ושחרור

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

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

<div id="target">You can drag an image file here</div>
<script>
  const target = document.getElementById('target');

  target.addEventListener('drop', (e) => {
    e.stopPropagation();
    e.preventDefault();

    doSomethingWithFiles(e.dataTransfer.files);
  });

  target.addEventListener('dragover', (e) => {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy';
  });
</script>

בדומה לקלט הקובץ, אפשר לקבל אובייקט FileList מהמאפיין dataTransfer.files של האירוע drop;

הגורם המטפל באירועים של dragover מאפשר לכם לאותת למשתמש מה יקרה אם הקובץ יושמט באמצעות הנכס dropEffect.

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

הדבקה מהלוח

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

<textarea id="target">Paste an image here</textarea>
<script>
  const target = document.getElementById('target');

  target.addEventListener('paste', (e) => {
    e.preventDefault();
    doSomethingWithFiles(e.clipboardData.files);
  });
</script>

(e.clipboardData.files הוא עוד אובייקט FileList.)

החלק הקשה ב-API של הלוח הוא, שכדי לקבל תמיכה מלאה בדפדפנים שונים, רכיב היעד צריך להיות גם ניתן לבחירה וגם לעריכה. גם <textarea> וגם <input type="text"> מתאימים לחיוב כאן, וכך גם רכיבים עם המאפיין contenteditable. אבל הם מיועדים גם מן הסתם לעריכת טקסט.

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

טיפול באובייקט FileList

מאחר שרוב השיטות שלמעלה יוצרות FileList, עלי להסביר קצת על מהותו.

FileList דומה ל-Array. יש לו מפתחות מספריים ומאפיין length, אבל הוא לא למעשה מערך. אין שיטות מערך, כמו forEach() או pop(), ואי אפשר לחזור עליהן. כמובן שניתן לקבל מערך אמיתי על ידי שימוש בפונקציה Array.from(fileList).

הערכים של FileList הם File אובייקטים. המאפיינים האלה זהים בדיוק לאובייקטים מסוג Blob, אלא שיש להם מאפיינים נוספים לקריאה בלבד מסוג name ו-lastModified.

<img id="output" />
<script>
  const output = document.getElementById('output');

  function doSomethingWithFiles(fileList) {
    let file = null;

    for (let i = 0; i < fileList.length; i++) {
      if (fileList[i].type.match(/^image\//)) {
        file = fileList[i];
        break;
      }
    }

    if (file !== null) {
      output.src = URL.createObjectURL(file);
    }
  }
</script>

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

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

  • משרטטים אותו לתוך רכיב <canvas> כדי שניתן יהיה לטפל בו
  • הורדה למכשיר של המשתמש
  • מעלים אותו לשרת עם fetch()

גישה למצלמה באופן אינטראקטיבי

עכשיו, לאחר שסגרתם את הבסיס, הגיע הזמן להשתפר בהדרגה!

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

קבלת גישה למצלמה

אפשר לגשת ישירות למצלמה ולמיקרופון באמצעות ממשק API במפרט של WebRTC שנקרא getUserMedia(). הפעולה הזו תבקש מהמשתמש גישה למיקרופונים ולמצלמות המחוברים.

התמיכה ב-getUserMedia() די טובה, אבל היא עדיין לא נמצאת בכל מקום. באופן ספציפי, היא לא זמינה ב-Safari מגרסה 10 ומטה, שנכון למועד הכתיבה הוא עדיין הגרסה היציבה האחרונה. עם זאת, Apple הודיעה שהוא יהיה זמין ב-Safari 11.

עם זאת, הזיהוי של נתוני התמיכה הוא מאוד פשוט.

const supported = 'mediaDevices' in navigator;

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

עם זאת, כדי לקבל נתונים מהמצלמה צריך מגבלה אחת בלבד – video: true.

אם הפעולה מצליחה, ה-API מחזיר MediaStream שמכיל נתונים מהמצלמה, ואז אפשר לצרף אותו לרכיב <video> ולהפעיל אותו כדי להציג תצוגה מקדימה בזמן אמת, או לצרף אותו ל-<canvas> כדי לקבל צילום של תמונת מצב.

<video id="player" controls autoplay></video>
<script>
  const player = document.getElementById('player');

  const constraints = {
    video: true,
  };

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

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

צילום תמונת מצב

האפשרות הנתמכת ביותר ליצירת תמונה היא לצייר מסגרת מהסרטון על קנבס.

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

זהו התהליך:

  1. צריך ליצור אובייקט על קנבס שישמור את המסגרת מהמצלמה
  2. קבלת גישה לשידור של המצלמה
  3. צירוף לרכיב וידאו
  4. כדי לצלם פריים מדויק, צריך להוסיף את הנתונים מרכיב הווידאו לאובייקט של בד ציור באמצעות drawImage().
<video id="player" controls autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    // Draw the video frame to the canvas.
    context.drawImage(player, 0, 0, canvas.width, canvas.height);
  });

  // Attach the video stream to the video element and autoplay.
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

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

  • מעלים את הקובץ ישירות לשרת
  • אחסון באופן מקומי
  • החלת אפקטים מגניבים על התמונה

טיפים

אפשר להפסיק לשדר מהמצלמה כשלא צריך

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

כדי להפסיק את הגישה למצלמה, אפשר להפעיל את stop() בכל טראק וידאו לשידור שמוחזר על ידי getUserMedia().

<video id="player" controls autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    context.drawImage(player, 0, 0, canvas.width, canvas.height);

    // Stop all video streams.
    player.srcObject.getVideoTracks().forEach(track => track.stop());
  });

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // Attach the video stream to the video element and autoplay.
    player.srcObject = stream;
  });
</script>

בקשת הרשאה לשימוש במצלמה באופן אחראי

אם המשתמש לא נתן לאתר שלכם בעבר גישה למצלמה, ברגע שתתקשרו אל getUserMedia(), הדפדפן יבקש מהמשתמש להעניק לאתר שלכם הרשאת גישה למצלמה.

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

תאימות

למידע נוסף על הטמעת דפדפנים בניידים ובמחשבים:

מומלץ להשתמש גם ב-shim של adapter.js כדי להגן על אפליקציות מפני שינויים במפרט של WebRTC והבדלים בקידומת.

משוב