Javascript, Node.js, Total.js 29 мартa 2020 2 мин. 7168
Хочу поделиться с Вами своими наработками, которые использовал в реальном проекте пару месяцев назад. Проект был связан с вендинговым аппаратом и необходимо было внедрить недорогую камеру, для мониторинга в случае каких-либо происшествий.
Поэтому я подумал, что мой опыт покажется Вам интересным. Мои наработки можно использовать в других проектах, например:
Изначально требования были следующие, решение должно быть универсальным, все должно работать с любой 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 камеру, при этом приложение будет эффективно работать на мини-компьютерах, так как камера будет включаться, только тогда, когда к ней подключится пользователь.