Используем веб-камеру в проекте на Node.js

Javascript, Node.js, Total.js 29 мартa 2020 2 мин. 375


Хочу поделиться с Вами своими наработками, которые использовал в реальном проекте пару месяцев назад. Проект был связан с вендинговым аппаратом и необходимо было внедрить недорогую камеру, для мониторинга в случае каких-либо происшествий.

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

  • Smart House (Умный дом) - всегда можно зайти на устройство и посмотреть все ли в порядке с квартирой или домом.
  • OpenCV (Распознавание образов) - популярная тема в данный момент, данные с камеры будут поступать, остаётся их только распознать.
  • Другие проекты, где необходима веб камера.

Изначально требования были следующие, решение должно быть универсальным, все должно работать с любой USB веб камерой и на любой ОС (Windows/Linux). Поэтому я быстро ознакомился с разными решениями, проверил несколько библиотек с использованием утилиты FFMPEG, а также с использованием мультимедийного фреймфорка GStreamer. В конечном итоге больше понравился GStreamer, так как конечное решение можно масштабировать, к тому же в npm репозитарии есть нативная библиотека, реализующая широкие возможности.


Фреймворк GStreamer

GStreamer

Мультимедийный фреймворк, написанный на языке программирования C и использующий систему типов GObject. GStreamer является «ядром» мультимедийных приложений, таких, как видеоредакторы, потоковые серверы и медиа проигрыватели.

Перед использование нам нужно установить данный фреймворк.


Шаг 1

В качестве фреймфорка для бэкенда будем использовать Totaljs. Для начала установим фреймворк следующей командой.

npm install total.js

Фреймворк из npm репозитария будет установлен в папку node-modules.
Также нам понадобится модуль gstreamer-superficial.

npm install gstreamer-superficial

Который реализует нативную привязку для работы с мультимедийный фреймфорком GStreamer.

Сразу хочу сказать, что могут возникнуть проблемы при установке на Windows. Установка и сборка легче если Вы будете использовать последнии версии Node.js, начиная с 12. Для Linux никаких проблем не выявил.

Создадим несколько папок в нашем проекте. Немного о структуре папок, которые я создал на сервере.

  • conrollers - в этой папке размещаем описание маршрутов (рут)
  • definations - здесь размещаем описание определений для фреймворка, этим самым мы будем менять поведение фреймворка
  • modules - здесь находится различные функциональные модули, которые расширяют возможности нашего приложения
  • public - в этой папке будем размещать css, javascript, изображения, в общем дополнительные ресурсы, которые нужны нашему приложению в браузере
  • views - в ней находятся шаблоны, которые необходимы фреймворку для рендеринга на стороне сервера, там будет находится всего один файл index.html, эта будет именно та страница, которую выдаст наш сервер при обращении.

Шаг 2

Шаблон страницы

Сразу же я сверстал страницу, на которой будет отображаться изображение с камеры и сделал кнопку "Сделать снимок".

@{layout('')}
@{title(config.name)}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=10" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />
    <!--Импортирует статические файлы -->
    @{import('meta', 'head', 'spa.js', 'app.css', 'favicon.ico')}
</head>
<body>
 <div class='container'> 
    <div class='row block-content m10'>         
        <div class='text-center'>
            <!--Для отображения изображения с камеры -->
            <canvas id="canvas" class='hidden'></canvas>
            <!--Для получения изображения будем обращаться к руте -->
            <img id='stream' src='/cam/stream.jpg' class='m10'><br>
            <!--Кнопка -->
            <button data-jc="click" data-jc-path="getScreen" data-loading-text="<i class='fa fa-spinner fa-spin'></i> Ждем..." autocomplete="off" type="button" class='btn btn-primary btn-lg mv20'><i class="fa fa-camera"></i> Сделать снимок</button>
        </div>         
    </div>    
  </div> 
</div>     
@{import('app.js')}
<!--Дополнительные библиотеки для создания снимков с камеры -->
<script src="/js/filesaver/canvas-toBlob.js" type="text/javascript"></script>
<script src="/js/filesaver/filesaver.min.js" type="text/javascript"></script>
</body>
</html>

Ресурсы

В папке public разместим наши ресурсы css и javascript. Это исходники jQuery, библиотека jComponent, а также библиотеки для сохранения изображения на диск.


Сжатие и оптимизация ресурсов для клиента

Теперь в папку definitions добавим файл merge.js.

// Css
MERGE('/css/app.css', '/css/ui.css', '/css/style.css', '/css/indent.css');
// JavaScript
MERGE('/js/app.js', '/js/jquery-2.2.4.min.js', '/js/jctajr.min.js', '/js/ui.js', '/js/app.js');

Дело в том, что в Total.js встроен минификатор, который минифицирует и объединяет javascript и css файлы. Это позволит уменьшить в размере ресурсы, которые будут передаваться на клиент.


Шаг 3

Маршруты

Создадим два маршрута. Один из них для доступа к приложению. Второй для отдачи изображения с камеры. При обращении к изображению, мы будем создавать pipeline и через некоторый интервал времени (500 мс) отправлять изображения в формате jpeg. Камера работать постоянно у нас не будет, так как если использовать на микрокомпьютерах типа Orange Pi или Rasberry Pi, то она может потреблять достаточно много ресурсов, поэтому камера будет включаться, как только мы зашли на определенную страницу, а дезактивировать будем по событию close, как только пользователь закроет страницу.

exports.install = function() {
    ROUTE('/*', 'index');
    F.file('/cam/stream.jpg',  stream_cam);    
}

//функция создаёт поток и начинает передавать изображения
function stream_cam(req, res) {
    var fs = require('fs');    
    var mimeType = 'image/jpeg'; 
    const boundary='boundarydonotcross'; 
    var appsink;   
    function pull() {
        appsink.pull(function(buf, caps) {
            if (caps) {                
                mimeType = caps['name'];
            }
            if (buf) {                
                res.write('--'+boundary+'\r\n');
                res.write('Content-Type: ' + mimeType + '\r\n' + 'Content-Length: ' + buf.length + '\r\n');
                res.write('\r\n');                
                res.write(buf, 'binary');                
                res.write('\r\n');                
                pull();                
            } else {
                //интервал передачи изображений
                setTimeout(pull, 500);
            }            
        })    
    } 
    //создаём модуль для работы с камерой
    var Cam = MODULE('Cam');
    var pipeline = Cam.pipeline();
    //активируем ресурс с названием sink
    appsink = pipeline.findChild('sink');    
    //play
    pipeline.play();           
    //начинаем передавать
    pull();                
    //передадим заголовок
    res.writeHead(200, {
        'Server': 'Node-GStreamer-MPEGStreamer',
        'Connection': 'close',
        'Expires': 'Fri, 01 Jan 2000 00:00:00 GMT',
        'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
        'Pragma': 'no-cache',
        'Content-Type': 'multipart/x-mixed-replace; boundary=' + boundary
    });  
    //событие, если ресурс будет закрыт, то выключим камеру
    res.on('close', function() {
        pipeline.stop();           
        Cam.pipeline_remove();             
    });    
}

Модуль

В папке modules создадим файл cam.js со следующим содержимым:

exports.name = 'Cam';
exports.version = '1.0';

//настройки камеры, частота кадров и разрешение
var param = { framerate:  "100/1", resolution: "800x640" };
    pipeline = undefined; //видеоканал  
var _this = this;    
//linux
//var init_cmd = 'v4l2src ! videoscale ! clockoverlay halignment=right valignment=bottom text="{0}, {1} " shaded-background=true font-desc="Sans, 18" ! video/x-raw, width={2}, height={3} ! videorate ! video/x-raw, framerate={4} ! jpegenc ! appsink name=sink';
//windows
var init_cmd = 'ksvideosrc device-index=0 ! videoscale ! clockoverlay halignment=right valignment=bottom text="{0}, {1} " shaded-background=true font-desc="Sans, 18" ! video/x-raw, width={2}, height={3} ! videorate ! video/x-raw, framerate={4} ! jpegenc ! appsink name=sink';

//создаём pipeline
exports.pipeline = function() {
    if (pipeline) return pipeline;
    return create_pipeline();    
}
//удаляем pipeline
exports.pipeline_remove = function() {
    pipeline = undefined;    
}

function create_pipeline() {
    const GStreamer = require('gstreamer-superficial');
    var resolution = param.resolution.split('x');    
    //субтитры (текст поверх видео)
    pipeline = new GStreamer.Pipeline(init_cmd.format('cam is test', new Date().format('dd.MM.yyyy'), resolution[0], resolution[1], param.framerate));          
    return pipeline;
}

Данный модуль очень простой, использует возможности библиотеки gstreamer-superficial и реализует несколько функций. Создание pipeline и удаление pipeline_remove. При созданиии потока используются команда init_cmd, которую перед использованием форматируем init_cmd.format(). Обратите внимание, что для разной ОС используется разный формат команды.


Шаг 4

Теперь осталось написать обработчик при нажатии на кнопку "Сделать снимок". Для этого создадим файл app.js разместим его на сервере в папке public/js со следующим содержимым:

function getScreen(e) {
    //копируем изображение на canvas
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    var image = document.getElementById('stream');
    canvas.width = image.width;
    canvas.height = image.height;
    ctx.width = image.width;
    ctx.height = image.height;
    ctx.drawImage(image, 0, 0, image.width, image.height);
    //сохраняем с canvas в файл
    canvas.toBlob(function(blob) {
      saveAs(blob, (new Date().format('dMyy_Hms'))+"_cam.jpg");
    });    
}

Заключение

В итоге у меня получилось приложение, которое может работать на любой ОС и использовать любую USB Web камеру, при этом приложение будет эффективно работать на мини-компьютерах, так как камера будет включаться, только тогда, когда к ней подключится пользователь.

Изображение с Web камеры
Изображение с Web камеры и кнопка "Сделать снимок"

Ссылки:

Категории