Capturing Audio & Video in HTML5

HTML5 Rocks

This article discusses APIs that are not yet fully standardized and still in flux. Be cautious when using experimental APIs in your own projects.

Введение

Захват потокового видео и аудио является приоритетным направлением в разработке веб-приложений на протяжении многих лет. В течение долгого времени для выполнения этих задач мы полагались на плагины (Flash и Silverlight). Come on!

На помощь пришел HTML5. Возможно, этот момент не очевиден, но распространение HTML5 подняло вопрос доступа к аппаратным ресурсам. Прекрасными примерами этого являются функции Geolocation (GPS), Orientation API (акселерометр), WebGL (графические процессоры) и API веб-аудио (звуковое оборудование). Эти разработки очень функциональны. Они созданы на основе API JavaScript высокого уровня и опираются на аппаратные возможности исходного устройства.

Эта статья посвящена новому API под названием navigator.getUserMedia(), который предоставляет веб-приложениям доступ к видеокамере и микрофону.

История появления getUserMedia()

История появления API getUserMedia() довольно интересна.

За последние несколько лет появилось несколько разных 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 – один из первых примеров реализации этого API. Чтобы увидеть его в действии, посмотрите это видео.
  • Chrome для Android (0.16)

Я не рекомендую использовать этот API, если только вы не работаете с одним из перечисленных выше браузеров. Разработчики постепенно переходят к getUserMedia(), и в перспективе вряд ли кто-нибудь захочет заниматься реализацией захвата медиаданных с помощью HTML.

Этап 2: элемент устройства

Многие считали, что в API захвата медиаданных в HTML слишком много ограничений, поэтому появилась новая спецификация, поддерживающая любые устройства, включая те, что появятся в будущем. Неудивительно, что эта разработка потребовала введения нового элемента <device>, который стал шагом на пути к созданию API getUserMedia().

Одним из первых браузеров, в котором была реализована поддержка захвата видео с помощью элемента <device>, стала Opera. Вскоре после этого (точнее, в тот же день), сообщество WhatWG решило отказаться от тега <device> в пользу более успешного API JavaScript под названием navigator.getUserMedia(). Через неделю в новую сборку Opera была добавлена поддержка обновленной спецификации getUserMedia(). В конце этого года компания Майкрософт присоединилась к разработке, выпустив экспериментальный пакет для 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> не был реализован ни в одном браузере. Таким образом, его можно не принимать в расчет, хотя у тега <device> все же было два преимущества. Во-первых, он имел семантический характер, а во-вторых, позволял не ограничиваться аудио- и видеоустройствами.

Переходим к следующей технологии.

Этап 3: WebRTC

Элемент <device> в конечном счете полностью исчез.

В последние несколько месяцев попытки создания API для захвата данных мультимедиа начали приносить ощутимые результаты – во многом благодаря проекту WebRTC (передача данных в Интернете в реальном времени). Разработку этой спецификации курирует специальная рабочая группа W3C WebRTC. В настоящее время над реализацией ее поддержки в браузерах работают такие известные компании, как Google, Opera, Mozilla, а также ряд других.

WebRTC имеет прямое отношение к getUserMedia() – первому API в этом наборе. Эта технология обеспечивает доступ к потоковым данным с микрофона и видеокамеры.

Поддержка

WebRTC можно включить в браузере Chrome 18.0.1008 и его более поздних версиях на странице about:flags.

Начало работы

Благодаря navigator.getUserMedia() наконец появилась возможность перехватывать данные с веб-камер и микрофонов непосредственно, то есть без помощи плагинов. Доступ к камере теперь осуществляется по запросу, и для этого ничего не нужно устанавливать. Эта возможность встроена в браузер, и это довольно интересно.

Включение

API getUserMedia() появился совсем недавно, поэтому он реализован только в сборках для разработчиков Google и Opera. В Chrome 18+ его можно включить на странице about:flags.

Включение getUserMedia() на странице about:flags браузера Chrome.

Для Opera вы можете загрузить одну из экспериментальных сборок для Android или обычных компьютеров.

Определение возможностей

Эта проверка позволяет убедиться в поддержке браузером функции navigator.getUserMedia.

function hasGetUserMedia() {
  // Note: Opera builds are unprefixed.
  return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia || navigator.msGetUserMedia);
}

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

Получение доступа к устройству ввода

Для доступа к веб-камере или микрофону необходимо запросить разрешение. Первый параметр метода getUserMedia() определяет тип данных, к которым запрашивается доступ. Например, если чтобы обратиться к веб-камере, необходимо задать для него значение video. Чтобы использовать одновременно камеру и микрофон, нужно указать video, audio.

<video autoplay></video>

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

  // Not showing vendor prefixes.
  navigator.getUserMedia('video, audio', 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.
    };
  }, onFailSoHard);
</script>

Итак, что же происходит далее? Функция захвата данных мультимедиа – прекрасный пример совместной работы различных API в HTML5. Она используется вместе с другими элементами HTML5: <audio> и <video>. Обратите внимание: мы не добавляем атрибут src или элементы <source> в тег <video>. Вместо того чтобы указывать URL файла мультимедиа, мы передаем URL элемента Blob из объекта LocalMediaStream, который представляет веб-камеру.

Мы также добавляем для тега <video> атрибут автовоспроизведения autoplay, чтобы видео не остановилось на первом же кадре. Добавить элементы управления также несложно.

Внимание! В Chrome есть неполадка: если передать в качестве значения параметра только audio, код не работает (crbug.com/112367). Та же проблема с тегом <audio> обнаружена и в Opera.

В Opera и Chrome реализованы различные версии этой спецификации, что несколько усложняет ее практическое использование на данном этапе.

Chrome

Ниже приведен фрагмент кода для Chrome 18+ (необходимо сначала включить эту функцию на странице about:flags).

navigator.webkitGetUserMedia('audio, video', function(localMediaStream) {
  var video = document.querySelector('video');
  video.src = window.webkitURL.createObjectURL(localMediaStream);
}, onFailSoHard);

Opera:

Сборки Opera для разработчиков основаны на обновленной версии спецификации. Ниже приведен фрагмент кода для Opera.

navigator.getUserMedia({audio: true, video: true}, function(localMediaStream) {
  video.src = localMediaStream;
}, onFailSoHard);

Существует несколько ключевых различий.

  • В getUserMedia() не используются префиксы.
  • Объект передается как первый аргумент, а не список строк.
  • Значение video.src присваивается непосредственно объекту LocalMediaStream, а не URL данного Blob. Насколько мне известно, в Opera в конечном итоге будет использоваться URL объекта Blob.

Кросс-браузерная поддержка

Ниже приведен пример (довольно ненадежный), который поддерживается в обоих браузерах.

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

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

Обязательно ознакомьтесь со статьей Майка Тейлора (Mike Taylor) и Майка Робинсона (Mike Robinson) gUM Shield. Она поможет устранить противоречия в вариантах кода для разных браузеров.

Безопасность

В будущем в браузерах при вызове метода getUserMedia(), возможно, будет появляться информационная панель, позволяющая пользователю предоставлять или запретить доступ к камере или микрофону. К сожалению, в спецификации уделено мало внимания вопросам безопасности. Пока что такая панель с разрешениями не реализована.

Обеспечение обратной совместимости

Для тех, кто не может воспользоваться методом 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);
}

Демонстрация основных возможностей

Снимки экрана

В API <canvas> есть метод ctx.drawImage(video, 0, 0), который позволяет легко выводить кадры <video> на элемент <canvas>. Конечно, теперь, когда появилась возможность ввода видео с помощью API getUserMedia(), можно легко создать приложение типа "фотокиоск" для работы с потоковым видео.

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

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 18. In other browsers, this 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;
}, onFailSoHard);

Применение эффектов

Фильтры CSS

Фильтры CSS в настоящее время поддерживаются в "ночных" сборках WebKit и Chrome 18+.

С помощью фильтров CSS к элементу <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>

Нажмите видео, чтобы ознакомиться с фильтрами CSS

Текстуры WebGL

Один из замечательных примеров использования функций захвата видео – вывод получаемых в режиме реального времени данных в качестве текстуры WebGL. Я абсолютно не разбираюсь в WebGL, поэтому предлагаю вам ознакомиться с руководством и демонстрационным примером Джерома Этьена (Jerome Etienne). В нем идет речь о том, как с помощью метода getUserMedia() и скрипта Three.js выводить потоковое видео в WebGL.

Использование getUserMedia с API веб-аудио

В этом разделе описаны возможные усовершенствования и улучшения этого API.

У меня есть мечта: встроить AutoTune в браузер, используя только открытые веб-технологии. Она почти осуществима. Уже есть API getUserMedia() для ввода звука с микрофона. Достаточно добавить эффекты реального времени с помощью API веб-аудио, и все будет готово. Недостает лишь интеграции этих двух решений (crbug.com/112404), хотя предварительное предложение уже разрабатывается.

Передача сигнала микрофона в API веб-аудио, возможно, будет когда-нибудь происходить так, как показано ниже.

var context = new window.webkitAudioContext();

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

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

Чтобы увидеть, как API getUserMedia() связывается с API веб-аудио, зайдите на сайт crbug.com/112404.

Выводы

В целом удаленный доступ к устройствам через Интернет всегда был достаточно сложной задачей. Многие пробовали решить ее, но мало кому удавалось это сделать. Большинство решений никогда не выходило за рамки проприетарного программного обеспечения, поэтому они не получили широкого распространения.

Настоящая проблема заключается в том, что модель безопасности для веб-приложений сильно отличается от традиционной. Лично мне не хотелось бы, чтобы доступ к моей камере был у первого попавшегося сайта. Решить этот вопрос довольно сложно.

Объединенные платформы, например PhoneGap, помогли раздвинуть границы возможного, но это лишь начало, и такое решение является временным. Чтобы веб-приложения могли успешно конкурировать с традиционными решениями, им необходим доступ к аппаратному обеспечению компьютеров.

API getUserMedia() является первой попыткой доступа к новому типу устройств. Надеюсь, что в ближайшем будущем появятся и другие решения.

Дополнительные ресурсы

Демонстрационные примеры

Comments

0