Video-streaming в Raspberry PI + WebRTC — победа?

3c7036afaa4cef700131ef781712d601.jpg

Небольшая предыстория

Я занимаюсь разработкой роботов (как хобби) уже долгое время, и столкнулся с проблемой передачи видео через интернет со своего 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 будет выглядеть таким образом:

Сервис —



        
                
        
        
        











        
        
        

Клиент (веб тестовая страница) —



        
                
        
        
                
        
        
        

Как видно, разница только в 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 или :3001/main и через какое то время должно появиться видео.

Что касаемо производительность — она много лучше, чем в моей первой реализации чисто на node js.

Вот пара метрик -

1280х720 видео, все процессы запущены. Робот подключен к интернету и выполняется код на стороне робота (доп нагрузка)

1280×720 видео, все процессы запущены. Робот подключен к интернету и выполняется код на стороне робота (доп нагрузка)

1280х720. Робот не выполняет код

1280×720. Робот не выполняет код

Стоит также отметить, что изменение разрешения видео (что очевидно) влияет на загрузку.

Этот код также был протестирован на Raspberry Pi Zero 2.

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

В планах — использовать TensorflowJS.

© Habrahabr.ru