Pull to refresh

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

Level of difficultyMedium
Reading time6 min
Views3.8K

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

Я занимаюсь разработкой роботов (как хобби) уже долгое время, и столкнулся с проблемой передачи видео через интернет со своего Raspberry PI 4 и Raspberry PI zero.

Сначала идея была в реализации WebRTC на node js, про что я написал в этой статье. Как было написано, проблема заключалась в высокой загрузке процессора.

WebRTC и Ghrome.

Chrome имеет высокую производительность, особенно его реализация WebRTC это что то.

В какое то время мне попалась статья на медиуме, в которой поднимался такой же вопрос, который меня мучает уже несколько лет — ссылка.

Довольно странный способ, но если перфоманс действительно такой, то почему бы и нет?

Реальная ситуация

После проверки этого способа возникла уже другая проблема - хромиум не видит камеру. так как версия ОС другая, плюс прошло уже немало времени. В добавок ко всему этому, способ, описанный у linux-project уже не работает так как поменялась апи камеры в Raspberian.

Но и тут можно решить эту проблему - создав виртуальную камеру, используя gststreamer, про это хорошо написано в этом топике.

Пример рабочего решения

Итак, решение, которое я собрал воедино, следующее:

  • Создаем виртуальную камеру, используя 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.

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

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

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

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

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

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

Tags:
Hubs:
Total votes 7: ↑6 and ↓1+10
Comments27

Articles