Javascript, Node.js, Total.js 29 мартa 2020 2 мин. 5624
Хочу поделиться с Вами своими наработками, которые использовал в реальном проекте пару месяцев назад. Проект был связан с вендинговым аппаратом и необходимо было внедрить недорогую камеру, для мониторинга в случае каких-либо происшествий.
Поэтому я подумал, что мой опыт покажется Вам интересным. Мои наработки можно использовать в других проектах, например:
Изначально требования были следующие, решение должно быть универсальным, все должно работать с любой USB веб камерой и на любой ОС (Windows/Linux). Поэтому я быстро ознакомился с разными решениями, проверил несколько библиотек с использованием утилиты FFMPEG
, а также с использованием мультимедийного фреймфорка GStreamer
. В конечном итоге больше понравился GStreamer
, так как конечное решение можно масштабировать, к тому же в npm репозитарии есть нативная библиотека, реализующая широкие возможности.
Мультимедийный фреймворк, написанный на языке программирования C
и использующий систему типов GObject. GStreamer является «ядром» мультимедийных приложений, таких, как видеоредакторы, потоковые серверы и медиа проигрыватели.
Перед использование нам нужно установить данный фреймворк.
-devel-
. Иначе у нас не будет кодеков, которые необходимы для установки пакета gstreamer-superficial
из npm.В качестве фреймфорка для бэкенда будем использовать 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
, эта будет именно та страница, которую выдаст наш сервер при обращении.Сразу же я сверстал страницу, на которой будет отображаться изображение с камеры и сделал кнопку "Сделать снимок".
@{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
файлы. Это позволит уменьшить в размере ресурсы, которые будут передаваться на клиент.
Создадим два маршрута. Один из них для доступа к приложению. Второй для отдачи изображения с камеры. При обращении к изображению, мы будем создавать 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()
. Обратите внимание, что для разной ОС используется разный формат команды.
Теперь осталось написать обработчик при нажатии на кнопку "Сделать снимок". Для этого создадим файл 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 камеру, при этом приложение будет эффективно работать на мини-компьютерах, так как камера будет включаться, только тогда, когда к ней подключится пользователь.