Так как я раньше уже использовал getUserMedia для захвата звука с микрофона, я подумал, что с видео тоже не будет никаких проблем, но они все же вылезли на свет. Т.е. проблем с самим захватом видео-потока не было, а вот с одновременным выводом данных с нескольких источников на одной странице оказалось не все так просто, как хотелось.
Итак, начнем с самого начала, а именно с захвата и вывода видео с одного источника. Для этого мы будем использовать ф-ю getUserMedia, которая поддерживается во всех нормальных браузерах старших версий (Stream API), ну разумеется кроме IE.
Пояснения
- Все примеры кода ниже будут писаться на angularjs, ибо сейчас пишу на нем.
- Все скрипты будут написаны для работы с браузерами Chrome и Opera, ниже будет написано почему.
getUserMedia
Для доступа к веб-камере необходимо запросить у пользователя разрешение, и тут на сцену выходит getUserMedia, она принимает три аргумента:
- constraints — тут мы указываем, к какому типу данных мы хотим получить доступ. Его мы рассмотрим ниже более подробно;
- successCallback — функция возвращает объект LocalMediaStream, это и есть наш поток с камеры;
- errorCallback — функция отрабатывает, если при попытке захвата потока происходит ошибка или если пользователь отказался предоставить доступ к своему устройству.
В качестве средства вывода мы будем использовать элемент video, в атрибут src которого будет передаваться URL элемент в формате Blob из объекта LocalMediaStream.
В результате, самая простая ф-я для захвата потока будет выглядеть так:
//Кусок из директивы navigator.webkitGetUserMedia({'video': true}, function (stream) { var video = document.createElement("video"); video.src = window.URL.createObjectURL(stream); video.controls = true; video.play(); angular.element(document.querySelector('body')).append(video); }, function (e) { alert("Ошибка при доступе к камере!"); });
Тут происходит следующее:
- Мы создаем элемент video;
- При помощи ф-и createObjectURL из объекта LocalMediaStream мы создаем URL элемент типа Blob, который передаем в качестве источника в элемент video;
- Разрешаем авто-воспроизведение;
- Вставляем наш созданный элемент на страницу.
Задача минимум решена, мы вывели поток с одной камеры на свою страничку. Теперь нам надо вывести потоки с остальных наших камер.
MediaStreamTrack
Конечно же, в попытках решить свою проблему, я обратился за помощью к объекту MediaStreamTrack, который представляет собой интерфейс для работы с потоками со всех мультимедийных устройств, до которых браузер смог добраться. MediaStreamTrack пока довольно-таки редкий зверь и встречается в последних версиях Chrome, Opera и Firefox. Так зачем же он нам нужен? А затем, чтобы получить информацию об источниках данных.
В общем, мы нащупали путеводную нить для решения нашей задачи. Только я почувствовал радость от сбывающейся мечты, как я понял, что не могу получить все источники разом для их вывода. После истерического поиска решения было установлено, что в Chrome и Opere объект MediaStreamTrack имеет ф-ю getSources, которая и является нашим спасением. Как видно из названия, эта ф-я возвращает объект, который содержит в себе информацию обо всех источниках аудио и видео.
Ну так найдем наши камеры:
getMediaSources: function () { var mediaSources = []; MediaStreamTrack.getSources(function (sources) { an.forEach(sources, function (val, key) { if (sources[key].kind === 'video') { mediaSources.push(val); } }); }); }
Объект sources, который нам предоставила ф-я getSources, представляет из себя массив объектов с информацией об источниках данных. Каждый из этих объектов содержит следующую информацию:
- id — уникальный идентификатор источника, генерируется браузером;
- kind — тип, к которому относится источник (audio или video);
- label — метка устройства (источника), в моём случае там было USB Video Device;
- facing — как я понял, параметр имеет значение только для мобильных платформ и указывает на переднюю и заднюю камеру (Принимает два значения User — фронт-камера и environment — задняя камера).
Решение
Таким образом, подведем итог того, что мы теперь умеем. Мы можем получить список всех источников с идентификаторами источников, а так же можем перехватывать данные с них и выводить. Осталось только сложить это все воедино, и мы получим то, к чему стремились.
Последовательность действий у нас будет такая:
- При загрузке страницы при помощи ф-и MediaStreamTrack.getSources мы определяем все источники видео сигнала;
- Выводим список источников на страницу. Делаем мы это для того, что нам все-таки придется давать разрешение на доступ к каждой камере. Этого можно избежать в том случае, если страница работает через https
- При нажатии на какой-либо источник из списка, мы перехватываем данные с него при помощи GetUserMedia, создаем элемент video для него и выводим. (Если выбираем один и тот же источник несколько раз, то просто будет делаться копия потока)
Перед тем как привести окончательный рабочий пример, мы вернемся к ф-и webkitGetUserMedia, а именно к её первому аргументу constraints. В документации написано, что туда передаются типы источников в формате:
{"video": true,"audio":true}
Нам этого явно мало, ведь нам надо как минимум передать идентификатор источника. Оказывается, что вместо стандартного объекта можно передать так называемый ограничительный объект. Благодаря ему, мы можем настроить довольно-таки много параметров, таких как частота кадров и разрешение.
var constraints = {}; constraints.video = { mandatory: { minWidth: 640, minHeight: 480, minFrameRate: 30 }, optional: [ { sourceId: sourceid } ] };
Наш ��бъект делится на две части:
- mandatory — тут указываются обязательные ограничения для нашего видео и, если они не могут быть выполнены, то будет вызвано исключение.
- optional — это не обязательные параметры, которые при возможности будут применены к потоку (т.е. если мы тут укажем, что на выходе мы хотим иметь видео сигнал с частотой кадров не 30, а 60, и наша камера обеспечивает такой поток, то мы получим то, что хотим, а если камера не удовлетворяет условиям, то видео будет выводиться с частотой 30 кадров, что будет соответствовать значению параметра minFrameRate в блоке mandatory).
Из параметров, которые можно настраивать я нашел такие:
- frameRate — частота кадров
- aspectRatio — соотношение сторон
- minWidth — минимальная ширина
- minHeight — минимальная высота
- sourceId — уникальный идентификатор источника
- width
- height
Теперь все готово для того, чтобы написать окончательный вариант нашего модуля:
Код модуля
/** * Created by abaddon on 11.09.14. */ /*global window, document, angular, MediaStreamTrack, console, navigator */ (function (w, d, an, mst, nav) { "use strict"; angular.module("camersRoom", []). value("$sectors", {}). directive("ngVideoSector", ['$sectors', function ($sectors) { return { restrict: "A", link: function (scope, elem, attr) { $sectors[attr.ngVideoSector] = elem; } }; }]). directive("ngRoomPlace", ["$room", "$sectors", "$compile", function ($room, $sectors, $compile) { return { restrict: "A", controller: function ($scope, $element) { this.createViews = function (html) { var videoBlock = $sectors.rec, content; videoBlock.append(html); content = videoBlock.contents(); $compile(content)($scope); }; }, link: function (scope, elem, attr, cont) { if ($room.support) { var mediaSources = [], html, count; $room.getMediaSources().then(function (sources) { an.forEach(sources, function (val, key) { if (sources[key].kind === 'video') {/*find only video devices. Отбираем только видео устройства*/ mediaSources.push(val); } }); count = mediaSources.length; if (count) { html = $room.createSourcePreview(mediaSources); cont.createViews(html); } else { scope.error = { show: true, text: "Ну для работы надо хоть одну камеру подключить!" }; } /*create video block views.*/ }); } else { scope.error = { show: true, text: "Очень жаль, но ваш браузер никуда не годится. Откройте Google Chrome" }; } } }; }]). factory("$room", ["$q", "$sectors", function ($q, $sectors) { var Room = function () { var methods = { get support() { return !!this.media; }, set support(value) { this.media = value; } }; an.extend(this, methods); this.support = mst.getSources; }; Room.prototype = { _createVideoElement: function (stream) { var video = d.createElement("video"); video.src = w.URL.createObjectURL(stream); video.controls = true; video.play(); $sectors.place.append(video); }, getMediaSources: function () {/*get all media sources. Получение всех медиа аудио, видео устройств*/ var defer = $q.defer(); mst.getSources(function (sources) { defer.resolve(sources); }); return defer.promise; }, createSourcePreview: function (mediaSources) { var htmlString = '', i = 0; an.forEach(mediaSources, function (val) { i++; htmlString += '<button class="video-preview" ng-click="startBroadcast($event)" id="' + val.id + '">Камера ' + i + '</button>'; }); return htmlString; }, addVideoPlace: function (sourceid) { var constraints = {}; constraints.video = { mandatory: { minWidth: 640, minHeight: 480, minFrameRate: 30 }, optional: [ { sourceId: sourceid } ] }; nav.webkitGetUserMedia(constraints, function (stream) { this._createVideoElement(stream); }.bind(this), function (e) { alert("Ошибка при получении потока с камеры!"); }); } }; return new Room(); }]); }(window, document, angular, MediaStreamTrack, navigator));
Приводить код html — шаблона я не буду. Все можно посмотреть на демке и на github.
На этом все, спасибо за внимание, и надеюсь, что эта статья будет кому-нибудь полезна.
Ну и список литературы конечно:
- www.w3.org/TR/mediacapture-streams
- www.blaccspot.com/blog/webrtc/tutorials/getusermedia-with-resolution-constraints-tutorial
- developer.mozilla.org/en-US/docs/Web/API
- www.sitepoint.com/introduction-getusermedia-api
- w3c.github.io/mediacapture-main/getusermedia.html#idl-def-Constraints
- muaz-khan.blogspot.ru
