getUserMedia (구버전)

출처: https://www.html5rocks.com/ko/tutorials/getusermedia/intro/
2016년 10월 글이라 현재와는 맞지 않을 수 있지만 getUserMedia의 역사를 알기 위해서 해당 아티클을 정리함


몇 년 동안 캡쳐를 하기 위해서 Flash나 Silverlight 같은 플러그인들을 사용할 수 밖에 없었다.
HTML5의 등장으로 인해 위치정보 (GPS), Orientation API (가속도계), WebGL (GPU), Web Audio API (오디오 장비) 등 하드웨어 장치 접근에 대한 큰 파장을 가지고 왔다.

여기에서는 사용자의 카메라와 마이크를 웹앱에서 접근할 수 있게 해주는 새로운 API인 navigator.getUserMedia()를 소개한다.

getUserMedia()로 가는 길

지난 몇 년 동안 Media Capture APIs의 여러 변종들이 발달해왔고
많은 사람들이 웹에서 기본 하드웨어 장치에 접근해야 한다는 것을 깨달았지만,
그것은 새로운 사양에다 온갖 것을 다 집어 넣는 방향이 되어버렸다.
모든 것들이 지저분해져서 결국 W3C가 엄청난 양의 제안들을 통합하고 정리하는 워킹그룹을 만들기로 결정했다.

Round 1: HTML Media Capture

HTML Media Capture는 웹에서의 미디어 캡쳐 표준으로
<input type="file">를 오버로딩하여 accept에 새로운 값을 추가하는 것으로 진행되었다.
<input type="file" accept="image/*;capture=camera">: 웹캠으로 캡쳐를 하려면, capture=camera을 설정하면 된다.

비디오 또는 오디오를 녹화하는 것도 비슷하다.
<input type="file" accept="video/*;capture=camcorder">
<input type="file" accept="audio/*;capture=microphone">

하지만 이 특별한 API는 실시간 효과를 처리하기에는 부족했고
HTML Media Capture는 단지 미디어 파일을 녹화하거나 그 순간 사진을 찍는 것만 가능하다.

Round 2: device element

HTML Media Capture에 대한 많은 의견들은 너무 제한적이었다는 것이다.
그래서 (향후에 나올) 어떤 종류의 기기에서도 지원되는 새로운 스펙이 등장했고,
getUserMedia()의 조상이 되는 <device> element라는 형태로 설계된다.

1
2
3
4
5
6
7
<device type="media" onchange="update(this.data)"></device>
<video autoplay></video>
<script>
function update(stream) {
document.querySelector("video").src = stream.url;
}
</script>

하지만 WhatWG는 navigator.getUserMedia()라 부르는 새로운 Javascript API에 대한 찬성으로 <device>를 폐기한다.
<device>가 비록 없어졌지만 두 가지 위대한 일을 해냈는데:

  1. 의미론적(semantic)이었고,
  2. 단순 오디오/비디오 장비보다 많은 장비들을 지원할 수 있게 쉽게 확장할 수 있었다.

Round 3: WebRTC

<device>가 결국 없어졌고, WebRTC(Web Real Time Communications) 덕분에 적절한 캡쳐 API를 찾는데 가속이 붙게되었다.
getUserMedia()를 통해 사용자의 카메라와 마이크 Stream을 접근할 수 있는 수단을 제공한다.

시작하기

navigator.getUserMedia()를 통해 플러그인 없이 웹캠과 마이크 입력을 건드릴 수 있다.
카메라 접근은 브라우저 안에 포함되어 있기 때문에 그냥 호출하면된다.

기능 지원 확인

기능 지원 확인은 단순히 navigator.getUserMedia가 존재하는지 확인하면 된다.

1
2
3
4
5
6
7
8
9
10
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');
}

Modernizr를 사용하면 브라우저 벤더들의 prefix 차이 문제를 피할 수 있다.

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

입력장치의 접근 권한 얻기

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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>

여기에서 <video>요소에 src 속성을 설정하거나 <source> 요소들을 포함하고 있지 않는다.
미디어 파일의 비디오 URL을 삽입하는 대신에, 웹캠의 LocalMediaStream에서 얻은 Blob URL을 삽입한다.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);

Web Audio API와 함께 getUserMedia 사용하기

크롬은 getUserMedia()에서 실시간 효과들을 위한 Web Audio API로 실시간 마이크 입력을 지원한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
);

미디어 소스 선택하기

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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()를 호출시에 카메라와 마이크에 대한 접근을 허용 또는 거부할지에 대한 의견을 물어보는 상태바를 표시한다.

앱이 SSL(https://) 상에서 동작중이라면, 권한은 지속된다..

대체물 제공

API가 지원되지 않을때 또는 특정한 이유로 호출이 실패하였을때 이미 저장된 비디오 파일로 대체하는 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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(); // getUserMedia API를 지원하지 않을 때
} else {
navigator.getUserMedia({video: true}, success, fallback);
}

사진 찍기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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');
}
}

// snapshot 찍기 이벤트리스너 추가
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>에 여러가지 효과들을 적용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<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>

Web Audio API와 함께 getUserMedia 사용하기

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
Share