Небольшая предыстория
Я занимаюсь разработкой роботов (как хобби) уже долгое время, и столкнулся с проблемой передачи видео через интернет со своего Raspberry PI 4 и Raspberry PI zero.
Сначала идея была в реализации WebRTC на node js, про что я написал в этой статье — https://habr.com/ru/articles/749550/. Как было написано, проблема заключалась в высокой загрузке процессора.
WebRTC и Ghrome.
Chrome имеет высокую производительность, особенно его реализация WebRTC это что то.
В какое то время мне попалась статья на медиуме, в которой поднимался такой же вопрос, который меня мучает уже несколько лет — https://medium.com/@marcus2vinicius/webrtc-unlocking-high-performance-on-raspberry-server-with-javascript-for-3g-4g-connections-8d1048bc12ff
Довольно странный способ, но если перфоманс действительно такой, то почему бы и нет?
Реальная ситуация
После проверки этого способа возникла уже другая проблема — хромиум не видит камеру. так как версия ОС другая, плюс прошло уже немало времени. В добавок ко всему этому, способ, описанный у linux-project уже не работает так как поменялась апи камеры в Raspberian.
Но и тут можно решить эту проблему — создав виртуальную камеру, используя gststreamer, про это хорошо написано в этом топике — https://forums.raspberrypi.com/viewtopic.php?t=359204
Пример рабочего решения
Итак, решение, которое я собрал воедино, следующее —
-
Создаем виртуальную камеру, используя gststreamer
-
Запускаем localhost, который будет отдавать только веб страницу (можно также в нем реализовать сокет подключение и для передачи сигналов WebRTC и тп). Для тестирования буду использовать этот сервис и для передачи веб страницы для тестирования
-
Запускаем chromium-browser который будет переходить на страницу сервиса, создающего WebRTC
-
Тестируем и радуемся!
Создание виртуальной камеры
Для начала, устанавливаем gststreamer:
sudo apt-get install -y gstreamer1.0-tools gstreamer1.0-plugins gstreamer1.0-libcamera
Далее необходимо установить сервис v4l2loopback-dkms и активировать его:
sudo apt-get install -y v4l2loopback-dkms
Открываем файл
sudo nano /etc/modules-load.d/v4l2loopback.conf
И добавляем в него v4l2loopback
Теперь необходимо создать виртуальную камеру. Для этого открываем файл
sudo nano /etc/modprobe.d/v4l2loopback.conf
и добавляем туда
options v4l2loopback video_nr=8
options v4l2loopback card_label="Chromium device"
options v4l2loopback exclusive_caps=1
где video_nr=8
это номер видео девайса. Если в системе используется, укажите другой
Перезагружаем систему и проверяем ls /dev/ — тут в списке должна быть камера под указанным номером.
Для запуска виртуальной камеры используем команду:
gst-launch-1.0 libcamerasrc ! "video/x-raw,width=1280,height=1080,format=YUY2",interlace-mode=progressive ! videoconvert ! v4l2sink device=/dev/video8
И теперь можем получить Raspberry PI камеру из под хромиума.
Создание сервиса WebRTC
Для создания сервиса я так же буду использовать node js.
Мне также понадобится сокет соединение для передачи сигналов между пирами.
Код сервиса:
const path = require("path"); const express = require("express"); const app = express(); const server = require("http").createServer(app); const { Server } = require("socket.io"); const io = new Server(server, { cors: { origin: true, methods: ["GET", "POST"], transports: ["polling", "websocket"], }, allowEIO3: true, path: "/api/socket/", }); const port = process.env.PORT || 3001; //Здесь отдаем скрипты app.use('/static', express.static(path.join(__dirname, 'src/public'))) app.use('/static_web', express.static(path.join(__dirname, 'src_web/public'))) // Отдаем страницу сервиса, которая запусукается в хромиуме app.get("/service", function (req, res) { console.log('service') res.sendFile(path.join(__dirname, './src/index.html')); }); //Отдаем тестовую страницу app.get("/main", function (req, res) { console.log('main') res.sendFile(path.join(__dirname, './src_web/index.html')); }); server.listen(port); let serviceSocketId = null; let webSocketId = null; io.on("connection", (socket) => { //Эта часть для инициализации коммуникации сервис - клиент console.log("connect"); socket.on("init_service", (message) => { serviceSocketId = socket.id; }); socket.on("init_web", (message) => { webSocketId = socket.id; }); socket.on("message_from_service", (message) => { console.log('message_from_service', message); socket.to(webSocketId).emit("signal_to_web", message); }); socket.on("message_from_web", (message) => { console.log('message_from_web', message); socket.to(serviceSocketId).emit("signal_to_service", message); }); });
HTML будет выглядеть таким образом:
Сервис —
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> </head> <body> </body> <script src="./simplepeer.min.js"></script> <script type="importmap"> { "imports": { "socket.io-client": "https://cdn.socket.io/4.7.2/socket.io.esm.min.js", "simple-peer": "https://cdnjs.cloudflare.com/ajax/libs/simple-peer/9.11.1/simplepeer.min.js" } } </script> <script src="./static/script.js" type="module"></script> </html>
Клиент (веб тестовая страница) —
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> </head> <body> <video id="localVideo" autoplay muted="muted"></video> </body> <script type="importmap"> { "imports": { "socket.io-client": "https://cdn.socket.io/4.7.2/socket.io.esm.min.js", "simple-peer": "https://cdnjs.cloudflare.com/ajax/libs/simple-peer/9.11.1/simplepeer.min.js" } } </script> <script src="./static_web/script.js" type="module"></script> </html>
Как видно, разница только в video тэге.
Сами скрипты —
import { io } from "socket.io-client"; const socket = io('http://localhost:3001', { path: '/api/socket/', }); let config = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; const peer = new RTCPeerConnection(config); socket.on('connect', () => { socket.emit('init_service'); socket.on('signal_to_service', async (message) => { if (message.offer) { await peer.setRemoteDescription(new RTCSessionDescription(message.offer)); const answer = await peer.createAnswer(); await peer.setLocalDescription(answer); socket.emit('message_from_service', { answer }); navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => { peer.addStream(stream); }); } if (message.answer) { await peer.setRemoteDescription(message.answer); } if (message.iceCandidate) { await peer.addIceCandidate(message.iceCandidate); } }); }) peer.onicecandidate = (event) => { socket.emit("message_from_service", { iceCandidate: event.candidate }); };
И скрипт веб страницы —
import { io } from "socket.io-client"; // тут необходимо указать локальный ip адресс, если тестируется не на Raspberry PI const socket = io('http://localhost:3001', { path: '/api/socket/', }); let config = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; const peer = new RTCPeerConnection(config); socket.on("signal_to_web", async (message) => { if (message.answer) { await peer.setRemoteDescription(message.answer); } if (message.iceCandidate) { await peer.addIceCandidate(message.iceCandidate); } }); peer.onicecandidate = (event) => { socket.emit("message_from_web", { iceCandidate: event.candidate }); }; peer.ontrack = (event) => { const video = document.getElementById('localVideo'); if (video) { video.srcObject = event.streams[0]; video.play(); } }; const init = async () => { const offer = await peer.createOffer({ offerToReceiveVideo: true, }); await peer.setLocalDescription(offer); socket.emit("message_from_web", { offer }); }; socket.on('connect', () => { // После подключения к серверу, инициализируем пользователя и // отправляем оффер socket.emit('init_web'); init(); })
После создания всех необходимых файлов и запуска сервисов, можно запустить хромиум.
Тут важно отметить, что его можно запускать не только с GUI!
chromium-browser —no-sandbox —headless —use-fake-ui-for-media-stream —remote-debugging-port=9222 http://localhost:3001/service
После этого можно перейти по адресу — localhost:3001/main или <RaspberryPI-IP>:3001/main и через какое то время должно появиться видео.
Что касаемо производительность — она много лучше, чем в моей первой реализации чисто на node js.
Вот пара метрик —
Стоит также отметить, что изменение разрешения видео (что очевидно) влияет на загрузку.
Этот код также был протестирован на Raspberry Pi Zero 2.
P.S — кажется, что для меня это решение единственное, которое имеет низкую загрузку процессора и которое также позволяет добавлять различный функционал.
В планах — использовать TensorflowJS.
ссылка на оригинал статьи https://habr.com/ru/articles/828142/
Добавить комментарий