Capturing Audio & Video in HTML5

HTML5 Rocks

소개하는 글

오디오와 비디오 캡쳐는 오랜 기간동안, 웹 개발의 "성배" 같은 것이 었습니다. 몇 년 동안 우리가 캡쳐를 위해서는 (Flash 또는 Silverlight 같은) 플러그인들을 사용할 수 밖에 없었습니다. Come on!

HTML5는 구원자가 될 것입니다. 분명하지는 않지만 그래도 HTML5의 등장은 하드웨어 장치 접근에 대한 큰 파장을 가지고 왔습니다. 위치정보 (GPS), Orientation API (가속도계), WebGL (GPU), Web Audio API (오디오 장비) 등이 완벽한 예시입니다. 이 기능들은 말도 안되게 강력하고, 시스템의 꼭대기에서 높은 수준의 JavaScript API로 노출되어 있습니다.

이 튜토리얼에서는 사용자의 카메라와 마이크를 웹앱에서 접근할 수 있게 해주는 새로운 API, navigator.getUserMedia(), 를 소개합니다.

getUserMedia()로 가는 길

만약 이 과거를 모른다면 , getUserMedia() API로 가는 길은 흥미로운 이야기가 될 것 입니다.

지난 몇 년 동안 "Media Capture APIs"의 여러 변종들이 발달해왔습니다. 많은 사람들이 웹에서 기본 하드웨어 장치에 접근해야 한다는 것을 깨달았지만, 그것이 새로운 사양에다 온갖 것을 다 집어 넣는 방향으로 이끌었습니다. 모든 것들이 지저분해져서 결국 W3C가 워킹그룹을 만들기로 결정했습니다. 그들의 목적은? 미쳐가는 것을 정리하는 것! Device APIs Policy (DAP) Working Group 은 엄청난 양의 제안들을 통합하고 정리하는 일을 시작합니다.

2011년에 일어난 일들을 제가 정리해보겠습니다...

Round 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">

그럴싸 해 보이지 않습니까? 저는 특히 file 입력을 재사용하는 것이 마음에 들었습니다. 의미상 괜찮아 보입니다. 이 특별한 "API"는 실시간 효과를 처리하기에는 부족했습니다. (예를 들면, 실시간 웹캠데이터를 <canvas>에 그리는 것과 WebGL 필터들을 적용하는 것). HTML Media Capture는 단지 미디어 파일을 녹화하거나 그 순간 사진을 찍는 것만 가능합니다.

Support:

  • Android 3.0 browser - 첫번째 구현들 중 하나. this video에서 실제 동작을 확인해보시기 바랍니다.
  • Chrome for Android (0.16)
  • Firefox Mobile 10.0
  • iOS6 Safari and Chrome (부분적으로 지원)

Round 2: device element

HTML Media Capture에 대한 많은 의견들은 너무 제한적이었습니다, (향후에 나올) 어떤 장비들이 던 지원하기 위한 새로운 스팩이 등장합니다. 당연하게, getUserMedia()의 조상이 되는 <device> element라는 형태로 만들어집니다.

오페라는 <device>요소를 기반으로 비디오 캡쳐의 초기 구현을 만든 첫번째 브라우저들 중 하나입니다. 그이후 바로 (정확히 말하면 같은 날), WhatWG는 새롭게 등장한 신인에 대한 찬성으로 <device> 태그를 폐기합니다, 이 JavaScript API를 navigator.getUserMedia()라고 부릅니다. 일주일 뒤, Opera는 변경된 getUserMedia() 사양을 지원하는 새로운 빌드를 내놓습니다. 그해 말, Microsoft가 새로운 사양을 지원하는 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>

Support:

불행히도, <device>를 포함한 브라우저가 없습니다. 제 입장에서는 걱정거리 API 하나가 줄었습니다. :) <device>가 비록 없어졌지만 두 가지 위대한 일을 해냈습니다: 1.) 시맨틱이었고, 2.) 단순 오디오/비디오 장비의 보다 많은 장비들을 지원하기 위해 쉽게 확장할 수 있었습니다.

숨을 고르시기 바랍니다. 이제 빠르게 나갑니다!

Round 3: WebRTC

<device>요소가 결국 멸종의 길로 가버렸습니다.

WebRTC (Web Real Time Communications)의 큰 노력 덕분에 적절한 캡쳐 API를 찾는 속도는 가속이 붙었습니다. WebRTC 사양은 W3C WebRTC Working Group에서 감독하고 Google, Opera, Mozilla와 몇몇에서 구현을 했습니다.

getUserMedia()는 WebRTC와 관련이 있습니다. 왜냐하면 WebRTC API로 가는 통로이기 때문입니다. 사용자의 카메라와 마이크 스트림을 접근할 수 있는 수단을 제공합니다.

Support:

getUserMedia()는 Chrome 21, Opera 18, Firefox 17 이후 버전에서 지원됩니다.

시작하기

navigator.getUserMedia()를 사용하면, 드디어 플러그인 없이 웹캠과 마이크 입력을 건드릴 수 있습니다. 카메라 접근은 이제 설치하는 것이 아니라 호출하면 됩니다. 그냥 브라우저 안에 포함되어 있습니다. 아직 와닿지 않으십니까?

기능 지원 확인

기능 지원 확인은 단순히 navigator.getUserMedia이 존재하는지 확인하면 됩니다:

function hasGetUserMedia() {
  return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia || navigator.msGetUserMedia);
}

if (hasGetUserMedia()) {
  // Good to go!
} else {
  alert('getUserMedia() is not supported in your browser');
}

또한 getUserMedia를 감지하기 위해 Modernizr를 사용하면 벤더들의 prefix 차이 문제를 피할 수 있습니다:

if (Modernizr.getusermedia){
  var gUM = Modernizr.prefixed('getUserMedia', navigator);
  gUM({video: true}, function( //...
  //...
}

입력장치의 접근 권한 얻기

웹캠과 마이크에 접근하기 위해서는 권한을 요청해야 합니다. getUserMedia()의 첫번째 인자는 접근하려하는 미디어 별 상세와 요구사항들을 나타내는 객체입니다. 예를들어, 웹캠에 접근하려 한다면, 첫번째 파라미터는 반드시 {video: true}이어야 합니다. 마이크와 카메라 모두 사용하기 위해서는 {video: true, audio: true}를 전달합니다:

<video autoplay></video>

<script>
  var errorCallback = function(e) {
    console.log('Reeeejected!', e);
  };

  // Not showing vendor prefixes.
  navigator.getUserMedia({video: true, audio: true}, function(localMediaStream) {
    var video = document.querySelector('video');
    video.src = window.URL.createObjectURL(localMediaStream);

    // Note: onloadedmetadata doesn't fire in Chrome when using it with getUserMedia.
    // See crbug.com/110938.
    video.onloadedmetadata = function(e) {
      // Ready to go. Do some stuff.
    };
  }, errorCallback);
</script>

자 여기서 어떤 일들이 벌어지고 있습니까? 미디어 캡쳐는 새로운 HTML5 API를 함께 사용해보는 완벽한 예제입니다. 예제는 우리의 다른 HTML5 친구들(<audio>,<video>)과 결합되어 동작합니다. 여기에서 <video>요소에 src 속성을 설정하거나 <source> 요소들을 포함하고 있지 않는 것을 주목하시기 바랍니다. 미디어 파일의 비디오 URL을 삽입하는 대신에, 웹캠의 LocalMediaStream에서 얻은 Blob URL를 삽입합니다.

그리고 <video>autoplay도 알아두셔야 합니다, 그렇지 않으면 첫번째 프레임에서 화면이 멈추게 될 것입니다. 제어판을 추가하는 것도 여러분들이 원하는데로 동작할 것 입니다.

크로스 브라우저 상에서 동작하는 것을 보고 싶으시다면, 이것을 따라 해보시기 바랍니다:

navigator.getUserMedia  = navigator.getUserMedia ||
                          navigator.webkitGetUserMedia ||
                          navigator.mozGetUserMedia ||
                          navigator.msGetUserMedia;

var video = document.querySelector('video');

if (navigator.getUserMedia) {
  navigator.getUserMedia({audio: true, video: true}, function(stream) {
    video.src = window.URL.createObjectURL(stream);
  }, errorCallback);
} else {
  video.src = 'somevideo.webm'; // fallback.
}

미디어 제약조건 설정하기 (해상도, 높이, 너비)

getUserMedia()의 첫번째 인자는 또한 얻게될 미디어 스트림에 대해 더 많은 요구사항들(또는 제약조건들)을 표시할 수 있습니다. 예를 들어, 기본적인 비디오 접근 대신에 (즉 {vide: true}), HD 화질의 스트림을 추가적으로 요청할 수 있습니다:

var hdConstraints = {
  video: {
    mandatory: {
      minWidth: 1280,
      minHeight: 720
    }
  }
};

navigator.getUserMedia(hdConstraints, successCallback, errorCallback);

...

var vgaConstraints = {
  video: {
    mandatory: {
      maxWidth: 640,
      maxHeight: 360
    }
  }
};

navigator.getUserMedia(vgaConstraints, successCallback, errorCallback);

더 많은 설정들은 constraints API에서 확인하시기 바랍니다.

미디어 소스 선택하기

Chrome 30이후 버전에서, getUserMedia()는 비디오와 오디오 소스를 선택할 수 있는 MediaStreamTrack.getSources() API를 지원합니다.

이 예제를 보면 마이크와 카매라 중 마지막으로 발견된 것이 미디어 스트림 소스로 선택됩니다:

MediaStreamTrack.getSources(function(sourceInfos) {
  var audioSource = null;
  var videoSource = null;

  for (var i = 0; i != sourceInfos.length; ++i) {
    var sourceInfo = sourceInfos[i];
    if (sourceInfo.kind === 'audio') {
      console.log(sourceInfo.id, sourceInfo.label || 'microphone');

      audioSource = sourceInfo.id;
    } else if (sourceInfo.kind === 'video') {
      console.log(sourceInfo.id, sourceInfo.label || 'camera');

      videoSource = sourceInfo.id;
    } else {
      console.log('Some other kind of source: ', sourceInfo);
    }
  }

  sourceSelected(audioSource, videoSource);
});

function sourceSelected(audioSource, videoSource) {
  var constraints = {
    audio: {
      optional: [{sourceId: audioSource}]
    },
    video: {
      optional: [{sourceId: videoSource}]
    }
  };

  navigator.getUserMedia(constraints, successCallback, errorCallback);
}

어떻게 미디어 소스를 선택하는지에 대한 Sam Dutton의 훌륭한 데모를 확인하시기 바랍니다.

보안

일부 브라우저는 getUserMedia()를 호출시에 카메라와 마이크에 대한 접근을 허용 또는 거부할지에 대한 의견을 물어보는 상태바를 표시합니다. 안타깝게도 사양상에서 보안에 대한 부분은 매우 적습니다. Chrome의 권한 요청 메세지 예 입니다:

Permission dialog in Chrome
Permission dialog in Chrome

만약 앱이 SSL(https://) 상에서 동작중이라면, 권한은 지속됩니다. 즉, 사용자가 매번 허용/거부를 할 필요가 없습니다.

대체물 제공

getUserMedia()가 지원되지 않는 사용자들을 위해, API가 지원되지 않을때 또는 특정한 이유로 호출이 실패하였을때 이미 저장된 비디오 파일로 대체하는 방법이 있습니다:

// Not showing vendor prefixes or code that works cross-browser:

function fallback(e) {
  video.src = 'fallbackvideo.webm';
}

function success(stream) {
  video.src = window.URL.createObjectURL(stream);
}

if (!navigator.getUserMedia) {
  fallback();
} else {
  navigator.getUserMedia({video: true}, success, fallback);
}

Basic demo

사진 찍기

<canvas> API의 ctx.drawImage(video, 0, 0) 함수는 <video> 프레임들을 <canvas>로 쉽게 그려줍니다. 우리는 getUserMedia()를 통해 비디오 입력을 가지고 있기 때문에,쉽게 실시간 포토부스 어플리케이션을 만들 수 있습니다:

<video autoplay></video>
<img src="">
<canvas style="display:none;"></canvas>

<script>
  var video = document.querySelector('video');
  var canvas = document.querySelector('canvas');
  var ctx = canvas.getContext('2d');
  var localMediaStream = null;

  function snapshot() {
    if (localMediaStream) {
      ctx.drawImage(video, 0, 0);
      // "image/webp" works in Chrome.
      // Other browsers will fall back to image/png.
      document.querySelector('img').src = canvas.toDataURL('image/webp');
    }
  }

  video.addEventListener('click', snapshot, false);

  // Not showing vendor prefixes or code that works cross-browser.
  navigator.getUserMedia({video: true}, function(stream) {
    video.src = window.URL.createObjectURL(stream);
    localMediaStream = stream;
  }, errorCallback);
</script>

Applying Effects

CSS Filters

CSS Filters를 사용해서 <video>에 여러가지 재미있는 효과들을 적용할 수 있습니다:

<style>
video {
  width: 307px;
  height: 250px;
  background: rgba(255,255,255,0.5);
  border: 1px solid #ccc;
}
.grayscale {
  +filter: grayscale(1);
}
.sepia {
  +filter: sepia(1);
}
.blur {
  +filter: blur(3px);
}
...
</style>

<video autoplay></video>

<script>
var idx = 0;
var filters = ['grayscale', 'sepia', 'blur', 'brightness',
               'contrast', 'hue-rotate', 'hue-rotate2',
               'hue-rotate3', 'saturate', 'invert', ''];

function changeFilter(e) {
  var el = e.target;
  el.className = '';
  var effect = filters[idx++ % filters.length]; // loop through filters.
  if (effect) {
    el.classList.add(effect);
  }
}

document.querySelector('video').addEventListener(
    'click', changeFilter, false);
</script>

Click the video to cycle through CSS filters

WebGL Textures

비디오 캡쳐의 놀라운 사용 예는 WebGL Texture로 실시간으로 입력그리는 것 입니다. (다른 사람들에게는 쉽겠지만) 저는 WebGL에 관해 전혀 모르기 떄문에, Jerome Etienne의 튜토리얼데모를 확인하는 것을 추천해드립니다. 예제에서는 실시간 입력을 WebGL Texture로 그리기 위해 getUserMedia()Three.js를 어떻게 사용하는지 알려줍니다.

Web Audio API와 함께 getUserMedia 사용하기

내 꿈들중 하나는 오픈 웹기술만으로 브라우저상의 AutoTune을 만드는 것 입니다!

크롬은 getUserMedia()에서 실시간 효과들을 위한 Web Audio API로 실시간 마이크 입력을 지원합니다. 마이크 입력에서 Web Audio API로 연결하는 방법은 다음과 같습니다:

window.AudioContext = window.AudioContext ||
                      window.webkitAudioContext;

var context = new AudioContext();

navigator.getUserMedia({audio: true}, function(stream) {
  var microphone = context.createMediaStreamSource(stream);
  var filter = context.createBiquadFilter();

  // microphone -> filter -> destination.
  microphone.connect(filter);
  filter.connect(context.destination);
}, errorCallback);

Demos:

더 많은 정보는 Chris Wilson's post에서 확인하시기 바랍니다.

결론

일반적으로, 웹에서 장치에 접근하는 것은 힘든 일이 었습니다. 많은 사람들이 시도했고, 일부만이 성공했습니다. 대부분의 이전 아이디어들은 폐쇄적이거나 널리 적용되지 못하였습니다.

실제 문제는 웹의 보안 모델과 현실세계의 보안 모델이 매우 다르다는 것입니다. 예를 들어, 모든 Joe Shmoe web site가 내 비디오 카메라에 임의로 접근하는 것을 원하지 않을 것입니다. 그것을 허용하기란 정말 어려운 일입니다.

PhoneGap같은 중계 프레임워크는 경계를 만든 것에 도움을 줄 것이지만, 그건 임시방편일 뿐입니다. 웹앱이 데스크탑 앱과 경쟁하기 위해서는 기본장치로의 접근이 필요합니다.

getUserMedia()는 새로운 장치들에 접근하는 첫번째 파도일 뿐입니다. 저는 가까운 미래에 더 많은 것들을 계속 볼 수 있기를 기원합니다!

Additional resources

Demos

Comments

0