Как стать автором
Обновить

Вывод видео с нескольких web-камер на одной странице

Время на прочтение7 мин
Количество просмотров25K
Как-то раз я приуныл, делать ничего не хотелось, и тут я вспомнил, что в детстве мне сильно хотелось иметь пульт видео наблюдения, как у какого-то злодея из кино, который сидит в темной комнате и хохочет, наблюдая за беспомощными людишками, которые пытаются найти выход. Ну и освежив свои детские воспоминания, я решил воплотить их в жизнь, ну ту часть с пультом наблюдения, без людишек. И тут моим другом стал шагающий семимильными шагами HTML5, а если точнее Stream API.
Так как я раньше уже использовал 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("Ошибка при доступе к камере!");
    });

Тут происходит следующее:
  1. Мы создаем элемент video;
  2. При помощи ф-и createObjectURL из объекта LocalMediaStream мы создаем URL элемент типа Blob, который передаем в качестве источника в элемент video;
  3. Разрешаем авто-воспроизведение;
  4. Вставляем наш созданный элемент на страницу.

Задача минимум решена, мы вывели поток с одной камеры на свою страничку. Теперь нам надо вывести потоки с остальных наших камер.

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 — задняя камера).

Решение


Таким образом, подведем итог того, что мы теперь умеем. Мы можем получить список всех источников с идентификаторами источников, а так же можем перехватывать данные с них и выводить. Осталось только сложить это все воедино, и мы получим то, к чему стремились.

Последовательность действий у нас будет такая:
  1. При загрузке страницы при помощи ф-и MediaStreamTrack.getSources мы определяем все источники видео сигнала;
  2. Выводим список источников на страницу. Делаем мы это для того, что нам все-таки придется давать разрешение на доступ к каждой камере. Этого можно избежать в том случае, если страница работает через https
  3. При нажатии на какой-либо источник из списка, мы перехватываем данные с него при помощи 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.

На этом все, спасибо за внимание, и надеюсь, что эта статья будет кому-нибудь полезна.

Ну и список литературы конечно:
Теги:
Хабы:
+14
Комментарии10

Публикации

Истории

Работа

Ближайшие события