У меня была задача — передача видео с минимальной задержкой с 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/
Добавить комментарий