Висновок відео з декількох веб-камер на одній сторінці

Як-то раз я зажурився, робити нічого не хотілося, і тут я згадав, що в дитинстві мені дуже хотілося мати пульт відео спостереження, як у якогось лиходія кіно, який сидить у темній кімнаті і регоче, спостерігаючи за безпорадними людьми, які намагаються знайти вихід. Ну і, освіживши свої дитячі спогади, я вирішив втілити їх у життя, ну ту частину з пультом спостереження, без людців. І тут моїм другом став крокує семимильними кроками 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
},
додаткова: [
{
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
},
додаткова: [
{ 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.

На цьому все, спасибі за увагу і сподіваюся, що ця стаття буде комусь корисна.

Ну і список літератури звісно:


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.