Реализация WebRTC в Node JS. Передача видео с Raspberry PI до Web

от автора

У меня была задача — передача видео с минимальной задержкой с Raspberry Pi до веб-интерфейса моего робота. Причем необходима была реализация на Node JS.

В этой статье я расскажу как можно реализовать стриминг с Raspberry Pi до веб-страницы используя WebRTC и Node JS.

Немного об WebRTC

WebRTC позволяет устанавливать p2p соединение между пользователями и передавать друг другу данные.

Принципиальная блок-схема показана на рисунке ниже:

Картинка с простор интернета

Картинка с простор интернета

Как видно из блок-схемы, для начала осуществления соединения необходимо на одной стороне сформировать offer, передать его используя сигнальный сервер (сокет например) на другую сторону. Отвечающая сторона формирует answer и отправляет его в качестве ответа на offer. Также стороны обмениваются iceCandidate между собой — они формируются автоматически.

Входные данные

Для реализации WebRTC в Node JS я использовал библиотеку node-webrtc. Установить ее можно командой ‘npm i wrtc‘. Эта библиотека имеет мощный функционал и много примеров. В качестве сигнального сервера я использовал сокет соединение. Также условимся, что сторона клиента будет формировать offer.

Веб-приложение написано на React.

Что происходит на Raspberry Pi

Перед формированием соединения нам еще необходимо получить MediaStream с камеры компьютера. Эта часть была одна из самых сложных для поиска. Решение было обнаружено в примерах wrtc и на просторах интернета.

Сам Raspberry Pi имеет встроенную библиотеку rspividiyv, которая передает видео-данные в необходимом нам формате yuv420p.

Для запуска этого процесса будем использовать spawn в node js.

const width = 640; const height = 480;  const { spawn } = require('child_process');  const raspividProcess = spawn('raspividyuv', ['-t', '0','-w', ${width}, '-h', ${height}, '-fps','30', '-o', '-']);            raspividProcess.stdout.pipe(videoChunker);

Далее необходимо подписаться на этот стрим:

raspividProcess.stdout.pipe(videoChunker);

где videoChunkerобработчик входных данных — он группирует все в буффер, код приведу ниже.

const { Transform } = require('stream') class Chunker extends Transform {   constructor (size) {     super();     this.size = size;     this.buffers = [];     this.length = 0;   }    _transform (chunk, encoding, callback) {     this.buffers.push(chunk);     this.length += chunk.length;      if (this.length >= this.size) {       const all = Buffer.concat(this.buffers);       for (let i = 0; i <= all.length - this.size; i += this.size) {         this.push(all.slice(i, i + this.size));       }        const rest = all.slice(Math.floor(this.length / this.size) * this.size);       this.buffers = [rest];       this.length = rest.length;     }     callback();   }    _flush (callback) {     callback();   } }  const videoChunker = new Chunker(width*height*1.5);

Теперь начинается самое интересное — создание подключения.

Определим MediaStream и подпишемся на обновления videoChunker.

const { MediaStream, nonstandard } = require('wrtc'); const { RTCVideoSource } = nonstandard;  const videoStream = new MediaStream(); const videoSource = new RTCVideoSource(); const videoTrack = videoSource.createTrack();  videoChunker.on('data', (data) => {   const i420Frame = {     width,     height,     data: new Uint8ClampedArray(data)   };   videoSource.onFrame(i420Frame) });

Тут хочу заметить, что необходимо чтобы width и height были такими же, что определенны выше.

Таким образом мы будем обновлять наш MediaStream.

Далее определяем RTCPeerConnection.

const pc = new RTCPeerConnection();  pc.addTrack(videoTrack, videoStream); pc.addStream(videoStream);

И добавляем обмен данными. Тут используем socket. Его реализацию оставляю на вашу часть или могу описать в другой статье. Так как offer формирует веб-пользователь, нам нужно получать offer и iceCandidate а отправлять answer и iceCandidate с нашей стороны.

socket.on('fromClient', async (message) => {   if (message.offer) {     await pc.setRemoteDescription(message.offer);     const answer = await pc.createAnswer();     await pc.setLocalDescription(answer);      socket.emit('toClient', ({ answer }));   }    if (message.iceCandidate) {     pc.addIceCandidate(message.iceCandidate);   } });  pc.onicecandidate = (event) => {   socket.emit('toClient', ({ iceCandidate: event.candidate })) }

При получении offer мы вызываем setRemoteDescription и далее формируем answer и устанавливаем setLocalDescription.

Когда peer создает кандидатов отправляем их на сторону клиента (браузер). Теперь можно переходить на сторону клиента.

Что происходит в браузере

Наша цель — получить видео с Raspberry Pi. Для теста можно собрать приложение используя create-react-app. Кусок кода на React:

function App() {   const remoteRef = useRef(null);    useEffect(() => {     const peer = new RTCPeerConnection();      socket.on("toClient", async (message) => {       if (message.answer) {         await peer.setRemoteDescription(message.answer);       }        if (message.iceCandidate) {         await peer.addIceCandidate(message.iceCandidate);       }     });      peer.onicecandidate = (event) => {       socket.emit("fromClient", { iceCandidate: event.candidate });     };      peer.ontrack = (event) => {       remoteRef.current.srcObject = event.stream[0];       remoteRef.current.play();     };      const init = async () => {       const offer = await peer.createOffer();       await peer.setLocalDescription(offer);        socket.emit("fromClient", { offer });     };      init();   }, []);    return (     <div className="App">       <header className="App-header">         <video ref={remoteRef} id="video" muted/>       </header>     </div>   ); }  export default App;

Что тут происходит ? При инициализации компонента мы сразу создаем offer и передаем его на Raspberry Pi через сокет-соединение. Также нужно подписаться на сообщения с сокета и также добавить хандлеры для peer-соединения.

Подведем итоги

После всего сделанного должно получиться передавать видео напрямую с Raspberry Pi с минимальной задержкой.

Могу добавить, что существует такие библиотеки как uv4l и uv4l_raspicam у linux-projects. Моя же цель была использовать Node JS чтобы иметь доступ в кадру с видео-камеры робота и далее проводить с ним различный манипуляции.

Что можно привести в заключение — это прекрасно работает. Было проверено при множестве соединений. Единственный минус — библиотеке wrtc сильно загружает процессор! Даже при передаче данных через data-channel, как только происходит подключение то нагрузка на процессор возрастает.

Чтобы снизить загрузку были проведены различные эксперименты, такие как вынос в отдельный кластер и т.п. но результат такой же. На картинке ниже — загрузка процессора, в какие то моменты она доходит до 99%.

Загрузка процессора

Загрузка процессора

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

Спасибо за прочтение, жду ваших комментариев и предложений, возможно кто то сталкивался с таким.


ссылка на оригинал статьи https://habr.com/ru/articles/749550/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *