Захват кадров с WebRTC

В этой статье объясняется как использовать WebRTC для получения доступа к камере компьютера или мобильного устройства, и захвата кадров с их помощью. Ознакомтесь с примером, а затем узнайте как это работает.

Uz WebRTC balstīta attēla uztveršanas lietotne - kreisajā pusē un bez tīmekļa kameras uzņemšanas video straumē un poga

Перейдите непостредственно к коду на Github , при желании.

Разметка HTML

Наш HTML интерфейс состоит из двух секций : панель отображения видео потока, из которого будет производиться захват и панель отображения результата захвата. Каждая панель имеет свой элемент <div>, для облегчения стилизации и управления.

Первая панель слева содержит два компонента : элемент <video> , который будет получать поток, отводимый с камеры, и элемент  <button>, каторый будет использоваться пользователем для активации захвата видео кадра.

  <div class="camera">
    <video id="video">Video stream not available.</video>
    <button id="startbutton">Take photo</button>
  </div>

Все это просто, и мы увидим как они связаны между собой, когда обратимся к коду  JavaScript .

В разметке имеется элемент <canvas> , который сохраняет захваченный кадр, который может быть дополнительно обработан и конвертируется в выходной файл изображения. Элемент canvas является скрытым, в его стиле свойство display:none, во избежании поломки интерфейса, где пользователю совершенно не обязательно видеть служебные элементы.

Для отображения пользователю результата захвата кадра, в интерфейсе расположен элемент <img>.

  <canvas id="canvas">
  </canvas>
  <div class="output">
    <img id="photo" alt="The screen capture will appear in this box.">
  </div>

Вот и все, что касается HTML. Остальное - просто пух макета страницы и немного текста, предлагающего ссылку на эту страницу.

Код JavaScript

Посмотрим на  JavaScript code. Разобъем его на части, для упрощения объяснения.

Инициализация

Начнем с обертки всего скрипта в анонимную функцию, во избежании конфликтов глобальных переменных, затем инициализируем различные нужные  переменные.

(function() {
  var width = 320;    // Этим создадим ширину фотографии
  var height = 0;    // Это будет вычисляться на основе входящего потока

  var streaming = false;

  var video = null;
  var canvas = null;
  var photo = null;
  var startbutton = null;

Все переменные выше:

width
Какой бы не был размер входящего видео, мы намерены масштабировать результирующее изображение к ширине в 320 px.
height
Результирующая высота изображения будет вычисляться на основе переданной ширины и соотношению сторон потока с камеры.
streaming
Указывает на текущую активность видеопотока.
video
Будет содержать ссылку на элемент <video>  после загрузки страницы.
canvas
Содержит ссылку на элемент  <canvas> после загрузки страницы.
photo
Содержит ссылку на элемент  <img> после загрузки страницы.
startbutton
Содержит ссылку на элемент  <button> после загрузки страницы, используюется для старта захвата.

Функция  startup()

Функция startup() запускается, когда страница закончила загрузку, благодаря установке window.addEventListener(). Работа функции состоит в том, что бы запросить доступ у пользователя к его камере, инициализировать элемент  <img> в значение по умолчанию, и установить обработчики событий, необходимых для получения каждого видеокадра с камеры, запускать захват изображения, при нажатии на кнопку.

Получаем ссылки на элементы

Сначала, получим ссылки на основные элементы, доступ к которым нам необходим.

  function startup() {
    video = document.getElementById('video');
    canvas = document.getElementById('canvas');
    photo = document.getElementById('photo');
    startbutton = document.getElementById('startbutton');

Получаем медиапоток 

Следующая задача - получение медиапотока:

    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
    .then(function(stream) {
        video.srcObject = stream;
        video.play();
    })
    .catch(function(err) {
        console.log("An error occurred: " + err);
    });

Здесь мы вазываем метод  MediaDevices.getUserMedia() , запрашивая медиапоток без аудиопотока (audio : false). Он возвращает промис, на котором мы определяем методы успешного и не успешного выполнений.

Успешное выполнение промиса передает объект потока( stream ) в качестве параметра функции метода then()., который присваевается свойству srcObject элемента <video>, направляя поток в него.

Как только поток связан с элементом <video> ,  запускаем его воспроизведение, вызовом метода HTMLMediaElement.play().

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

Обработка события начала воспроизведения

После момента вызова метода  HTMLMediaElement.play() на элементе <video>, возникает промежуток времени до начала воспроизведения видеопотока. Для недопущения блокирования интерфейса пользователя в это промежуток, нужно установить обработчик события canplay элемента video , который сработает, когда элемент начнет воспроизведение видеопотока. В этот момент все свойства элемента video конфигурируются на основе формата потока.

    video.addEventListener('canplay', function(ev){
      if (!streaming) {
        height = video.videoHeight / (video.videoWidth/width);
      
        video.setAttribute('width', width);
        video.setAttribute('height', height);
        canvas.setAttribute('width', width);
        canvas.setAttribute('height', height);
        streaming = true;
      }
    }, false);

Функциональность обработчика не будет запущена, если он запускается повторно. Это отслеживает переменная streaming , которая содержит значение  false при первом запуске обработчика.

Если это действительно первый запуск, мы устанавливаем высоту видео на основе разницы в размере между фактическим размером видео - video.videoWidth и шириной, на которую мы собираемся его визуализировать - width

Наконец, свойства width и height элементов video и canvas устанавливаются так, чтобы соответствовать друг другу, вызывая метод Element.setAttribute() на каждом из двух свойств каждого элемента, и, при необходимости, устанавливая ширину и высоту. Наконец, мы установили для  переменной streaming значение true, чтобы предотвратить случайное повторное выполнение этого установочного кода.

Обработка нажатий кнопки

Для захвата кадра, пользователь каждый раз нажимает кнопку  startbutton, нужно добавить обработчик события кнопки, для его вызова при возникновении события  click :

    startbutton.addEventListener('click', function(ev){
      takepicture();
      ev.preventDefault();
    }, false);

Метод прост, он вызывает функцию  takepicture(), определяемую ниже в секции  Capturing a frame from the stream, затем вызывает метод Event.preventDefault() на полученном объекте события, для предотвращения действия обработки события более одного раза.

Завершение метода  startup() 

Еще пара строк кода в методе startup():

    clearphoto();
  }

Вызов метода clearphoto() описывается в секции Clearing the photo box.

Отчистка бокса для фотографии

Очистка бокса фотографии включает создание изображения, а затем преобразование его в формат, используемый элементом <img>, который отображает последний снятый кадр. Код ниже:

  function clearphoto() {
    var context = canvas.getContext('2d');
    context.fillStyle = "#AAA";
    context.fillRect(0, 0, canvas.width, canvas.height);

    var data = canvas.toDataURL('image/png');
    photo.setAttribute('src', data);
  }

Начнем с получения ссылки на скрытый элемент <canvas>, который мы используем для рендеринга за пределами экрана. Затем мы устанавливаем свойсто fillStyle в  #AAA ( светло-серый) и заполняем весь холст этим цветом, вызывая метод fillRect().

Наконец, в этой функции мы конвертируем canvas в изображение PNG и вызываем метод photo.setAttribute() отображая захваченный цветовой фон в элементе изображения (бокса для фотографии).

Захват кадра из видеопотока

Последняя функция, требующая определения и являющаяся основным смыслом всего примера - takepicture() , которая захватывает текущий видеокадр, конвертирует его в формат PNG, и отображает его в блоке отображения кадра. Код её ниже:

  function takepicture() {
    var context = canvas.getContext('2d');
    if (width && height) {
      canvas.width = width;
      canvas.height = height;
      context.drawImage(video, 0, 0, width, height);
    
      var data = canvas.toDataURL('image/png');
      photo.setAttribute('src', data);
    } else {
      clearphoto();
    }
  }

Как и в случае, когда нам нужно работать с содержимым canvas, мы начинаем с 2D drawing context  для скрытого canvas.

Затем, если ширина и высота не равны нулю (имеется в виду, что есть, по крайней мере, потенциально допустимые данные изображения), мы устанавливаем ширину и высоту canvas, чтобы они соответствовали ширине захваченного кадра, затем вызываем метод drawImage() , что бы отрисовать текущий кадр видео в контексте canvas, заполнив весь холст изображением кадра.

Примечание : Используется факт того, что интерфейс HTMLVideoElement похож на интерфейс HTMLImageElement для любых API , которые принимают HTMLImageElement в качестве параметра, с текущим кадром видео, представленным как содержимое изображения.

Как тоько  canvas будет содержать захваченное видео, конвертируем его в  PNG формат, вызывая метод HTMLCanvasElement.toDataURL() на нем; наконец вызываем метод photo.setAttribute() отображая захваченное изображение в элементе изображения (бокса фотографии).

Если подходящее изображение не доступно (то есть, width и height равны  0), отчищаем содержимое элемента изображения, вызывая метод clearphoto().

Приколы с фильтрами

Поскольку мы снимаем изображения с веб-камеры пользователя, захватывая кадры из элемента <video> , можно легко применить фильтры и забавные эффекты к элементу video. Оказывается, любые CSS-фильтры, которые вы применяете к элементу с помощью свойства filter, влияют на захваченную фотографию.Эти фильтры могут варьироваться от простых (делая изображение черно-белым) до экстремальных (размытие по Гауссу и вращение оттенка).

Вы можете экспериментировать с этими эффектами, используя, например, инструмент разработчика FirefoxYou  редактор стилей; смотрим Редактирование с CSS фильтрами о подробностях выполнения.

Использование определенных устройств

При необходимости вы можете ограничить набор разрешенных источников видео, определенным устройством или набором устройств. Для этого нужно вызвать метод navigator.mediaDevices.enumerateDevices(). Когда промис разрешиться массивом объектов MediaDeviceInfo , описывающих доступные устройства , выберите те, которым хотите разрешить доступ и укажите соответствующий идентификатор устройства deviceId или несколько deviceId в объекте  MediaTrackConstraints , переданном в  getUserMedia().

Смотри так же