如何使用 WebRTC 构建点对点Web视频通话应用

我们所有人都曾以某种形式使用过点对点视频通话应用程序。它可能是 zoom 会议、google meet 或 omegle。

最近,我正在学习 webRTC,发现这项技术在构建点对点通信应用程序时非常有用。请跟我一起了解它是如何工作的。

如何使用 WebRTC 构建点对点Web视频通话应用

什么是点对点通信?

点对点或 P2P 是一种通信方法,在这种方法中,2 台设备无需从中央服务器发送数据即可进行通信。这意味着当我们进行视频通话时,音频和视频数据不会发送到中央服务器。相反,数据是通过互联网从一个浏览器发送到另一个浏览器。

什么是 WebRTC?

WebRTC 是一种直接进行实时通信的创新技术。大多数现代浏览器都内置了对 WebRTC 的支持。WebRTC 允许用户通过互联网向另一个对等方传输任何形式的数据(原始数据/音频、视频)。

让我们来看看如何构建自己的视频通话Web应用程序。

步骤1:获取用于信号传输的 HTTP 服务器

有些人可能会问,既然是 P2P 通信,为什么还需要 HTTP 服务器?原因是两个用户之间的通信是点对点的,但我们需要以信号的形式从服务器发送一些数据给客户端。例如:当一个用户试图连接另一个用户时,我们需要通知另一个用户有来电。

一旦对等网络之间建立了通信,对等网络就不需要向 HTTP 服务器发送任何数据。

我使用 Express JS 创建了一个简单的 HTTP 服务器,用于提供静态 HTML 文件。

const http = require('http');
const express = require('express');
const {Server} = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

HOST = '0.0.0.0';
PORT = 3000;

app.use(express.static( path.resolve('./public') ));

app.get("/health", (req,res) => {
    res.send("Status: OK");
});

server.listen(PORT, HOST, () => {
    console.log(`Listening on port: ${PORT}`)
})

要执行信号传递,我们需要实现 websockets。为此,我们将使用 Socket.io。我们将用 Socket io 封装我们的 express 服务器,Socket io 库实现了 websockets,用于保持与客户端的连接。

let rooms = {};

io.on('connection', socket => {
    console.log(`User connected ${socket.id}`);

    socket.on('create-room', (roomId) => {
        // Logic to create a new room
        if(!rooms[roomId]) {
            rooms[roomId] = {
                members: [socket.id],
            };
            // socket.join(roomId);
            socket.emit('room-created', roomId);
            console.log(`Room created: ${roomId}`);

        } else {
            // Room already exists
            socket.emit('room-exists');
        }
    });
    
    socket.on('join-room', (roomId) => {
        // Logic to handle joining an existing room
        const room = rooms[roomId];

        if (room && room.members.length == 1) {
            // room.members.push(socket.id);
            // socket.join(roomId);
            console.log("Sending join request to room owner", socket.id);
            io.to(room.members[0]).emit('join-request', socket.id);
        } else {
            // Room is full or does not exist
            socket.emit('room-unavailable');
        }
    });

    socket.on('approve-join-request', (roomId, requesterUserId) => {
        console.log(roomId, requesterUserId);
        const room = rooms[roomId];
    
        if (room) {
            if(room.members[0]==socket.id) {
                room.members.push(requesterUserId);
                // socket.join(roomId);
                io.to(requesterUserId).emit('join-approved');
                console.log(`User ${requesterUserId} approved to join room ${roomId}`);
                io.to(requesterUserId).emit('start-peer-connection', socket.id);
            }
        }
    });
    
    socket.on('offer-request', data => {
        const { fromOffer, to } = data;
        console.log("Forwarding offer request to: "+ to);
        socket.to(to).emit('offer-request', { from: socket.id, offer: fromOffer });
    });

    socket.on('offer-answer', data => {
        const { answere, to } = data;
        console.log("Forwarding offer answer to: "+ to);
        socket.to(to).emit('offer-answer', { from: socket.id, offer: answere });
    });

    socket.on('peer-updated', data => {
        const { candidate, to } = data;
        console.log("Peer updated");
        socket.to(to).emit('peer-updated', { from: socket.id, candidate: candidate });
    });

    socket.on('disconnect', () => {
        console.log('User disconnected:', socket.id);
        // Handle user disconnection, leave rooms, etc.
      });
});

对于服务器上的每个套接字事件,我们都会执行一些操作。

例如:当用户要创建房间时,客户端会发送一条 websocket 消息 create-room。然后,我们执行验证并创建一个新房间。

您可以阅读代码了解每个事件。

现在信令服务器已经准备就绪,开始实现 p2p 通信。

我们将为客户端编写简单的 html 代码。将有 2 个选项–创建新房间或加入现有房间,还将在 html 中加入 2 个视频元素,用于显示视频流。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Peer-to-Peer Video Call</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body style="background-image: url('https://wallpapers.com/images/hd/abstract-background-6m6cjbifu3zpfv84.jpg');">

        <h1 class="text-3xl font-bold underline">
            P2P Video Call
          </h1>
          <br>
        <div class="grid grid-cols-3 gap-4">
            <div class="mb-6" style="width: 300px;">
                <input type="text" id="create-room-id" placeholder="Enter room id" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                <br>
                <button onclick="createRoom()" type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Create new room</button>
            </div>

            <div class="mb-6">
                <input type="text" id="join-room-id" placeholder="Enter room id" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                <br>
                <button onclick="joinRoom()" type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Join existing room</button>
            </div>

            <div class="relative overflow-x-auto" id="incoming-calls">
            </div>
        </div>

        <div class="grid grid-cols-2 gap-4">
            <div id="local-video-container">
                <video id="local-video" autoplay muted></video>
            </div>
        
            <div id="remote-video-container">
                <video id="remote-video" autoplay></video>
            </div>
        </div>

        <script src="/socket.io/socket.io.js"></script>
        <script src="client.js"></script>
    </body>
</html>

我们将使用简单的 javascript 来实现代码,可以从 socket.io 导入 socket io 客户端 javascript 库。

当用户点击创建新房间时,将向服务器发送创建房间信号。当用户点击 “加入房间 “时,将向服务器发送 “加入房间 “信号和房间 ID。

function createRoom() {
    roomId = document.getElementById('create-room-id').value;
    isRoomCreator = true;

    // Emit an event to the server to create a room
    socket.emit('create-room', roomId);
}

function joinRoom() {
    roomId = document.getElementById('join-room-id').value;

    // Emit an event to the server to join a room
    socket.emit('join-room', roomId);
}

对于每次加入房间事件,后台服务器都会发送处理数据,并向现有房间成员发送加入请求信号。当客户端收到加入请求信号时,会将收到的请求填充到表格中并显示出来。当现有用户接受加入请求时,客户端将向服务器发送批准加入请求信号。

socket.on('join-request', (requesterUserId) => {
    console.log("Join Request came", requesterUserId);
    displayJoinRequest(requesterUserId);
});

function displayJoinRequest(requesterUserId) {
    incomingCalls.push(requesterUserId);
    populateIncomingCalls();
}

function approveJoinRequest(requesterUserId) {
    console.log(requesterUserId);
    // Emit an event to the server to approve the join request
    socket.emit('approve-join-request', roomId, requesterUserId);
    // joinRequestsContainer.innerHTML = '';
    incomingCalls = incomingCalls.filter(function(item) {
        return item !== requesterUserId
    });
    populateIncomingCalls();
}

function rejectJoinRequest(requesterUserId) {
    // Implement rejection logic if needed
    // joinRequestsContainer.innerHTML = '';
    incomingCalls = incomingCalls.filter(function(item) {
        return item !== requesterUserId
    });
    populateIncomingCalls();
}

之后,服务器将向其中一个对等设备发送信号,开始视频通话。客户端将初始化一个对等对象,并创建一个 WebRTC 提议。要创建 WebRTC 提议,客户端需要知道有哪些方式可以从互联网上的其他计算机与之通信。为此,需要连接 STUN 或 TURN 服务器。连接 STUN 服务器后,它会告诉客户端从互联网到达客户端的方式。有许多免费的 STUN 服务器。我们将在本项目中使用谷歌提供的 STUN 服务器。

WebRTC offer 将包含 SDP 或关于该浏览器的描述。客户端将向服务器发送该请求,服务器将把该请求转发给加入同一房间的另一个对等方。

peer = new RTCPeerConnection({
    iceServers: [
        {
            urls: "stun:stun.l.google.com:19302"
        }
    ]
});

socket.on('start-peer-connection', (to) => {
    initializePeerConnection(to);
});

async function initializePeerConnection(remoteSocketId) {
    console.log("Remote Socket ID: "+ remoteSocketId);
    remoteSocketId = remoteSocketId;
    const localOffer = await peer.createOffer();
    console.log("Set local description for creating call: " + localOffer);
    await peer.setLocalDescription(new RTCSessionDescription(localOffer));
    socket.emit('offer-request', { fromOffer: localOffer, to: remoteSocketId });
}

其他对等点将读取该报价并将对等点提供的 SDP 设置为远程描述。该客户端将创建与初始报价相对应的应答报价并将其发送到服务器。服务器将此应答提议转发给原始对等方。原始对等方将读取应答提议并将 SDP 设置为远程描述。


socket.on('offer-request', async data => {
    const { from, offer } = data;
    console.log("Incoming offer for webRTC from:" + from);
    await peer.setRemoteDescription(new RTCSessionDescription(offer));
    console.log("Set Remote Description:" + offer);

    const answereOffer = await peer.createAnswer();
    console.log("Created answer offer:" + answereOffer);
    await peer.setLocalDescription(new RTCSessionDescription(answereOffer));
    console.log("Set Local Description:" + answereOffer);

    socket.emit('offer-answer', { answere: answereOffer, to: from });
    remoteSocketId = from;

    const mySteam = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true
    });

    mySteam.getTracks().forEach((track) =>
        peer.addTrack(track, mySteam));
});



socket.on('offer-answer', async data => {
    console.log("Received answer from peer");
    const { offer } = data;
    await peer.setRemoteDescription(new RTCSessionDescription(offer));
    console.log("Set Remote description after answer received from peer: ", offer);
});

现在,两个客户端都有对方的 SDP 作为远程描述。这意味着 WebRTC 通道已经建立。现在可以通过信道发送任何数据。

我们将开始在该信道上流式传输对等方的音频和视频。每当接收到对等方的视频轨道时,我们就会将其设置为远程视频对象。

const startVideoStream = async() => {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
        localVideo.srcObject = stream;

        // Add the local stream to the connection
        stream.getTracks().forEach((track) => peer.addTrack(track, stream));

    } catch (error) {
        console.error('Error Streaming video:', error);
    }
}



peer.ontrack = (event) => {
    // Don't set srcObject again if it is already set.
    if (remoteVideo.srcObject) return;
    remoteVideo.srcObject = event.streams[0];
};

现在,我们的 P2P 视频通话应用程序已经准备就绪。运行 npm start 并前往 http://localhost:3000/ 开始视频通话。

作者:Arnav Rupade
代码链接:https://github.com/ArnavRupde/p2p-video-call

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/38020.html

(0)

相关推荐

发表回复

登录后才能评论