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

소개

오디오/동영상 캡처는 오랫동안 웹 개발의 성배였습니다. 수년 동안 작업을 완료하려면 브라우저 플러그인 (Flash 또는 Silverlight)을 사용해야 했습니다. 어서요!

HTML5가 해결책입니다. 눈에 띄지 않을 수도 있지만 HTML5의 등장으로 기기 하드웨어에 대한 액세스가 급증했습니다. 위치정보 (GPS), 방향 API (가속도계), WebGL (GPU), Web Audio API (오디오 하드웨어)가 좋은 예입니다. 이러한 기능은 시스템의 기본 하드웨어 기능 위에 있는 상위 수준 JavaScript API를 노출하여 매우 강력합니다.

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

getUserMedia()로 가는 길

이전 버전을 알지 못한다면 getUserMedia() API에 도달한 방식은 흥미로운 이야기입니다.

지난 몇 년 동안 '미디어 캡처 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 Media Capture가 너무 제한적이라고 생각하여 모든 유형의 (향후) 기기를 지원하는 새로운 사양이 등장했습니다. 당연히 이 설계에는 새 요소인 <device> 요소가 필요했고, 이 요소는 getUserMedia()의 전신이 되었습니다.

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> 요소는 결국 사라졌습니다.

적합한 캡처 API를 찾는 속도는 더 큰 WebRTC (Web Real Time Communications) 노력 덕분에 가속화되었습니다. 이 사양은 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의 완벽한 예입니다. 이 기능은 다른 HTML5 친구인 <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);

자세한 구성은 제약 조건 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;
}