WebRTC (Интернет-связь в реальном времени) — это технология с набором функций, которые позволяют пользователю получать аудио- и видеоматериалы и передавать эту информацию при одноранговой связи. С помощью этого соединения также можно отправлять любые данные, такие как текст или файлы.
В этом посте представлено руководство по реализации простого обмена видео и чата без использования каких-либо библиотек или плагинов, помимо ресурсов WebRTC.
Структура проекта
Этот проект состоит из сервера, который работает как точка доступа для клиентов, запускающих веб-связь. WebSocket используется для того, чтобы клиенты могли узнавать друг друга.
Клиент представляет собой простой HTML-код для получения видео/аудиопотока и ввода для отправки сообщений чата. Связь WebRTC реализована в файле Javascript, импортированном этим HTML.
Ресурсы WebRTC
- MediaStream: представляет поток мультимедийного контента с дорожками для аудио и видео. Вы можете получить объект MediaStream с помощью функции navigator.mediaDevices.getUserMedia().
- RTCPeerConnection: представляет соединение между двумя одноранговыми узлами. Он используется для отправки потока между клиентами.
- RTCDataChannel: представляет двунаправленный канал данных между двумя парами соединения. Он используется для отправки сообщений чата между клиентами.
Покажи мне код
Начнем с кода сервера. Сначала мы начнем проект NodeJS.
yarn init -y
Установите необходимые зависимости. Express для создания сервера и socket.io для включения связи WebSocket.
yarn add express socket.io
Создайте server.js, чтобы запустить наш сервер, и поместите следующий код:
const express = require('express');
const socketio = require('socket.io');
const cors = require('cors');
const http = require('http');
// Create server
const app = express();
const server = http.Server(app);
// Enable Cors to Socket IO
app.use(cors());
// Init Socket IO Server
const io = socketio(server);
// Called whend a client start a socket connection
io.on('connection', (socket) => {
});
// Start server in port 3000 or the port passed at "PORT" env variable
server.listen(process.env.PORT || 3000,
() => console.log('Server Listen On: *:', process.env.PORT || 3000));
Начальная структура проекта должна быть примерно такой:]
Структура веб-сокета
Цель веб-сокета - сделать так, чтобы клиент не знал друг друга без соединения WebRTC.
Соединение WebRTC устанавливается в несколько шагов, описанных ниже. Все эти шаги объясняются в разделе реализации клиента.
- Создайте экземпляр RTCPeerConnection;
- Создайте Предложение для подключения;
- Отправьте Ответ на запрос предложения;
- Сигнализация между клиентами.
Итак, для реализации этого необходимо добавить некоторые события в socket.
Первый шаг — отправить себе других подключенных пользователей, чтобы запустить RTCPeerConnection с каждым из них. После этого у нас есть события для установления соединения со всеми шагами, описанными выше.
Ниже у нас есть полный код этой реализации.
// Array to map all clients connected in socket
let connectedUsers = [];
// Called whend a client start a socket connection
io.on('connection', (socket) => {
// It's necessary to socket knows all clients connected
connectedUsers.push(socket.id);
// Emit to myself the other users connected array to start a connection with each them
const otherUsers = connectedUsers.filter(socketId => socketId !== socket.id);
socket.emit('other-users', otherUsers);
// Send Offer To Start Connection
socket.on('offer', (socketId, description) => {
socket.to(socketId).emit('offer', socket.id, description);
});
// Send Answer From Offer Request
socket.on('answer', (socketId, description) => {
socket.to(socketId).emit('answer', description);
});
// Send Signals to Establish the Communication Channel
socket.on('candidate', (socketId, signal) => {
socket.to(socketId).emit('candidate', signal);
});
// Remove client when socket is disconnected
socket.on('disconnect', () => {
connectedUsers = connectedUsers.filter(socketId => socketId !== socket.id);
});
});
Код клиента
Сначала создайте папку с именем public и добавьте файлы index.html и main.js. Окончательная структура проекта должна выглядеть так:
- HTML-код
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebRTC Example</title>
<style>
#video-grid {
display: none;
grid-template-columns: repeat(auto-fill, 400px);
grid-auto-rows: 400px;
}
video {
width: 100%;
height: 100%;
}
</style>
<script src="/socket.io/socket.io.js"></script>
<script src="/main.js" type="module"></script>
</head>
<body>
<h1>Hello!</h1>
<!-- My Video and Remote Video from connection -->
<div id="video-grid">
<video playsinline autoplay muted id="local-video"></video>
<video playsinline autoplay id="remote-video"></video>
</div>
<!-- Input to send messages -->
<div>
<span style="font-weight: bold">Message: </span>
<input type="text" id="message-input" title="Message to Send!">
<button id="message-button">Send</button>
</div>
<!-- Area to Print Images -->
<div class="messages"></div>
</body>
</html>
В файле main.js первым шагом является запуск MediaStream, например:
console.log('Main JS!');
// Map All HTML Elements
const videoGrid = document.getElementById('video-grid');
const messagesEl = document.querySelector('.messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('message-button');
const localVideo = document.getElementById('local-video');
const remoteVideo = document.getElementById('remote-video');
// Open Camera To Capture Audio and Video
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
// Show My Video
videoGrid.style.display = 'grid';
localVideo.srcObject = stream;
// Start a Peer Connection to Transmit Stream
initConnection(stream);
})
.catch(error => console.log(error));
Результат примерно такой: ваше видео находится в области local-video.
Следующие шаги — запуск сокетного соединения и инициализация RTCPeerConnectin для подключения друг к другу пользователей. При получении события сокета other-users клиент инициирует соединение с каждым из них.
const initConnection = (stream) => {
const socket = io('/');
let localConnection;
let remoteConnection;
// Start a RTCPeerConnection to each client
socket.on('other-users', (otherUsers) => {
// Ignore when not exists other users connected
if (!otherUsers || !otherUsers.length) return;
const socketId = otherUsers[0];
// Ininit peer connection
localConnection = new RTCPeerConnection();
// Add all tracks from stream to peer connection
stream.getTracks().forEach(track => localConnection.addTrack(track, stream));
// Send Candidtates to establish a channel communication to send stream and data
localConnection.onicecandidate = ({ candidate }) => {
candidate && socket.emit('candidate', socketId, candidate);
};
// Receive stream from remote client and add to remote video area
localConnection.ontrack = ({ streams: [ stream ] }) => {
remoteVideo.srcObject = stream;
};
// Create Offer, Set Local Description and Send Offer to other users connected
localConnection
.createOffer()
.then(offer => localConnection.setLocalDescription(offer))
.then(() => {
socket.emit('offer', socketId, localConnection.localDescription);
});
});
}
ВАЖНО: в реальных условиях RTCPeerConnection должен быть инициализирован с настройками iceServers с серверами STUN и TURN, это необходимо получить реальный IP-адрес для подключения к Интернету и избежать блоков NAT в сети. Подробнее об этом смотрите в RTCPeerConnection и WebRTC в реальном мире
Продолжая наше руководство, теперь другой клиент получит запрос offer и должен создать RTCPeerConnection с вашим ответом.
// Receive Offer From Other Client
socket.on('offer', (socketId, description) => {
// Ininit peer connection
remoteConnection = new RTCPeerConnection();
// Add all tracks from stream to peer connection
stream.getTracks().forEach(track => remoteConnection.addTrack(track, stream));
// Send Candidtates to establish a channel communication to send stream and data
remoteConnection.onicecandidate = ({ candidate }) => {
candidate && socket.emit('candidate', socketId, candidate);
};
// Receive stream from remote client and add to remote video area
remoteConnection.ontrack = ({ streams: [ stream ] }) => {
remoteVideo.srcObject = stream;
};
// Set Local And Remote description and create answer
remoteConnection
.setRemoteDescription(description)
.then(() => remoteConnection.createAnswer())
.then(answer => remoteConnection.setLocalDescription(answer))
.then(() => {
socket.emit('answer', socketId, remoteConnection.localDescription);
});
});
Наконец, первый клиент получает ответ и устанавливает Удаленное описание. Итак, запустите кандидатов на отправку, чтобы создать канал связи для отправки потока.
// Receive Answer to establish peer connection
socket.on('answer', (description) => {
localConnection.setRemoteDescription(description);
});
// Receive candidates and add to peer connection
socket.on('candidate', (candidate) => {
// GET Local or Remote Connection
const conn = localConnection || remoteConnection;
conn.addIceCandidate(new RTCIceCandidate(candidate));
});
Конечный результат выглядит примерно так, как показано на изображении ниже, где показаны локальные и удаленные видео.
Ссылка
https://developer.mozilla.org/pt-BR/docs/Web/API/WebRTC_API
https://www.html5rocks.com/en/tutorials/webrtc/infrastructure/
Следующие шаги
Вы можете посмотреть весь код в GitHub
Следуйте следующему сообщению, чтобы настроить отправку сообщений чата и завершить это руководство.
Первоначально опубликовано на https://dev.to 11 ноября 2020 г.