HTML5에서 오디오 및 동영상 캡처

소개

오디오/동영상 캡처는 오랫동안 웹 개발의 '무한한 가능성'이었습니다. 여러 해 동안 이 작업을 수행하기 위해 브라우저 플러그인 (Flash 또는 Silverlight)을 사용해야 했습니다. 자, 이제 시작

HTML5가 해결해 줍니다. 분명하게 보이지 않을 수 있지만 HTML5의 등장으로 기기 하드웨어에 대한 액세스가 급증했습니다. Geolocation(GPS), Orientation API(가속도계), WebGL(GPU), Web Audio API(오디오 하드웨어)가 좋은 예입니다. 이러한 기능은 엄청나게 강력하여 시스템의 기본 하드웨어 기능 위에 있는 높은 수준의 JavaScript API를 노출합니다.

이 튜토리얼에서는 웹 앱이 사용자의 카메라와 마이크에 액세스할 수 있는 새로운 API인 GetUserMedia를 소개합니다.

getUserMedia()로 가는 길

getUserMedia() API가 탄생한 과정을 모르는 경우 흥미로운 이야기입니다.

지난 몇 년 동안 'Media Capture API'의 여러 변종이 발전했습니다. 많은 사람들이 웹에서 네이티브 기기에 액세스할 수 있어야 한다고 생각했지만, 이로 인해 모두가 새로운 사양을 작성하게 되었습니다. 상황이 너무 복잡해져서 W3C는 결국 작업 그룹을 구성하기로 결정했습니다. 유일한 목적은 무엇인가요? 광란의 시대를 이해하세요. 기기 API 정책(DAP) 작업반은 수많은 제안을 통합하고 표준화하는 작업을 담당했습니다.

2011년에 발생한 상황을 요약해 보겠습니다.

1단계: HTML 미디어 캡처

HTML 미디어 캡처는 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 미디어 캡처를 사용하면 미디어 파일을 녹화하거나 특정 시점에 스냅샷을 찍을 수만 있습니다.

지원:

  • Android 3.0 브라우저 - 초기 구현 중 하나입니다. 이 동영상에서 작동 방식을 확인하세요.
  • Android용 Chrome(0.16)
  • Firefox Mobile 10.0
  • iOS6 Safari 및 Chrome (부분 지원)

2단계: 기기 요소

많은 사람들이 HTML 미디어 캡처가 너무 제한적이라고 생각했기 때문에 모든 유형의 (향후) 기기를 지원하는 새로운 사양이 나타났습니다. 당연하게도 이 설계는 getUserMedia()의 전신인 새 요소인 <device> 요소를 요구했습니다.

Opera는 <device> 요소를 기반으로 동영상 캡처의 초기 구현을 만든 최초의 브라우저 중 하나였습니다. 그 직후(정확히 당일) WhatWG는 <device> 태그를 스크래핑하기로 하고 대신 navigator.getUserMedia()라는 JavaScript API를 사용하기로 했습니다. 일주일 후 Opera는 업데이트된 getUserMedia() 사양 지원이 포함된 새 빌드를 출시했습니다. 그해 말 Microsoft도 새로운 사양을 지원하는 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> 요소는 결국 도도새의 길을 걷게 되었습니다.

대규모 WebRTC(Web Real Time Communications) 작업 덕분에 적절한 캡처 API를 찾는 속도가 빨라졌습니다. 이 사양은 W3C WebRTC 작업 그룹에서 감독합니다. Google, Opera, Mozilla 및 기타 몇몇 업체에서 구현한 기능을 제공합니다.

getUserMedia()는 해당 API 집합으로 연결되는 게이트웨이이므로 WebRTC와 관련이 있습니다. 사용자의 로컬 카메라/마이크 스트림에 액세스하는 수단을 제공합니다.

지원:

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>

예. 어떻게 된 걸까요? 미디어 캡처는 새로운 HTML5 API가 함께 작동하는 완벽한 예입니다. <audio><video>와 함께 작동합니다. <video> 요소에 src 속성을 설정하거나 <source> 요소를 포함하지 않습니다. 동영상에 미디어 파일의 URL을 제공하는 대신 웹캠을 나타내는 LocalMediaStream 객체로 srcObject를 설정합니다.

또한 <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);

자세한 구성은 constraints API를 참고하세요.

미디어 소스 선택

MediaDevices 인터페이스의 enumerateDevices() 메서드는 마이크, 카메라, 헤드셋과 같은 사용 가능한 미디어 입력 및 출력 장치의 목록을 요청합니다. 반환된 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;
}