จับเสียงและวิดีโอในรูปแบบ HTML5

บทนำ

การบันทึกเสียง/วิดีโอเป็น "จอกศักดิ์สิทธิ์" ของการพัฒนาเว็บมาเป็นเวลานาน เป็นเวลาหลายปีที่เราต้องพึ่งพาปลั๊กอินเบราว์เซอร์ (Flash หรือ Silverlight) เพื่อทำงานให้เสร็จ เร็วเข้า

HTML5 ช่วยคุณได้ แม้ว่าอาจจะไม่ชัดเจน แต่การเพิ่มขึ้นของ HTML5 ทำให้การเข้าถึงฮาร์ดแวร์ของอุปกรณ์เพิ่มขึ้นอย่างรวดเร็ว ตำแหน่งทางภูมิศาสตร์ (GPS), Orientation API (ตัวตรวจวัดความเร่ง), WebGL (GPU) และ Web Audio API (ฮาร์ดแวร์เสียง) เป็นตัวอย่างที่สมบูรณ์แบบ ฟีเจอร์เหล่านี้มีประสิทธิภาพสูงมาก โดยแสดง JavaScript API ระดับสูงที่อยู่เหนือความสามารถของฮาร์ดแวร์พื้นฐานของระบบ

บทแนะนำนี้จะแนะนำ API ใหม่ GetUserMedia ซึ่งช่วยให้เว็บแอปเข้าถึงกล้องและไมโครโฟนของผู้ใช้ได้

เส้นทางสู่ getUserMedia()

หากคุณไม่ทราบประวัติความเป็นมา วิธีที่เราได้มาซึ่ง API getUserMedia() ถือเป็นเรื่องราวที่น่าสนใจ

"Media Capture API" หลายรูปแบบได้รับการพัฒนาในช่วงไม่กี่ปีที่ผ่านมา หลายคนตระหนักถึงความจำเป็นในการเข้าถึงอุปกรณ์ที่มาพร้อมเครื่องบนเว็บ แต่ก็ทำให้ทุกคนและแม่ของพวกเขาต้องร่วมกันสร้างข้อกำหนดใหม่ สิ่งต่างๆ ยุ่งเหยิงมากจนในที่สุด W3C ก็ตัดสินใจจัดตั้งคณะทำงานขึ้นมา จุดประสงค์เดียวของพวกเขาคืออะไร ทำความเข้าใจความบ้าคลั่งนี้ กลุ่มทำงานด้านนโยบาย API ของอุปกรณ์ (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 ช่วยให้คุณบันทึกไฟล์สื่อหรือถ่ายภาพสแนปชอตได้ในขณะนั้นเท่านั้น

การสนับสนุน:

รอบที่ 2: องค์ประกอบของอุปกรณ์

หลายคนคิดว่า HTML Media Capture มีข้อจำกัดมากเกินไป จึงเกิดข้อกำหนดใหม่ที่รองรับอุปกรณ์ (ในอนาคต) ทุกประเภท จึงไม่น่าแปลกใจที่การออกแบบนี้ ต้องใช้องค์ประกอบใหม่ นั่นคือองค์ประกอบ <device> ซึ่งกลายเป็นต้นแบบของ getUserMedia()

Opera เป็นหนึ่งในเบราว์เซอร์แรกๆ ที่สร้างการใช้งานครั้งแรกของการจับภาพวิดีโอโดยอิงตามองค์ประกอบ <device> หลังจากนั้นไม่นาน (ในวันเดียวกัน) WhatWG ตัดสินใจยกเลิกแท็ก <device> เพื่อหันไปใช้แท็กใหม่ที่กำลังมาแรง ซึ่งครั้งนี้เป็น JavaScript API ที่ชื่อว่า navigator.getUserMedia() หนึ่งสัปดาห์ต่อมา Opera ได้เปิดตัวบิลด์ใหม่ที่รองรับข้อกำหนด getUserMedia() ที่อัปเดตแล้ว และในปีเดียวกันนั้น Microsoft ก็ได้เข้าร่วมด้วยการเปิดตัว Lab สำหรับ 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> ก็มีข้อดีอยู่ 2 อย่างคือ 1.) เป็นเชิงความหมาย และ 2.) ขยายได้ง่ายเพื่อรองรับมากกว่าแค่อุปกรณ์เสียง/วิดีโอ

พักหายใจสักหน่อย ทุกอย่างจะเกิดขึ้นเร็วมาก

รอบที่ 3: WebRTC

องค์ประกอบ <device> จึงหายไปในที่สุด

การค้นหา API การจับภาพที่เหมาะสมนั้นรวดเร็วขึ้นเนื่องจากความพยายามที่มากขึ้นของ WebRTC (Web Real Time Communications) ข้อกำหนดเฉพาะดังกล่าวอยู่ภายใต้การดูแลของคณะทำงาน WebRTC ของ W3C Google, Opera, Mozilla และอื่นๆ อีก 2-3 รายมีการใช้งานแล้ว

getUserMedia() เกี่ยวข้องกับ WebRTC เนื่องจากเป็นเกตเวย์ไปยังชุด API ดังกล่าว ซึ่งเป็นวิธีเข้าถึงสตรีมกล้อง/ไมโครโฟนในเครื่องของผู้ใช้

การสนับสนุน:

getUserMedia() ได้รับการรองรับตั้งแต่ Chrome 21, Opera 18 และ Firefox 17

เริ่มต้นใช้งาน

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

ดูการสาธิตที่ยอดเยี่ยมของ Sam Dutton เกี่ยวกับวิธีให้ผู้ใช้เลือกแหล่งที่มาของสื่อ

ความปลอดภัย

เบราว์เซอร์จะแสดงกล่องโต้ตอบสิทธิ์เมื่อเรียกใช้ 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;
}