WebRTC现学现用

591 阅读6分钟

前言

项目中有个需求,要实现用户通过浏览器实时查看家中的摄像头视频,并对家中的摄像头进行控制。一听到这个功能,第一直觉反应就是应该用WebRTC技术实现。大方向没错,可是光知道大方向不行,功能实现方面还是要填充细节,之前对WebRTC是泛泛的了解,现在项目中要实际使用,得亲自实践一下才行。

什么是WebRTC?

WebRTC,全称为Web Real-Time Communication,是一项由W3C(万维网联盟)和IETF(互联网工程任务组)共同开发的技术。它的目标是使网络应用和网站能够在不依赖中介服务(如服务器或软件插件)的情况下,直接通过浏览器进行点对点的音视频和数据通信。

传统的音视频通信通常需要依赖专门的插件或应用程序,如Skype、Zoom等。这些工具虽然功能强大,但在实现上往往复杂,且用户需要安装额外的软件或插件。而WebRTC则打破了这一障碍,使得音视频通信和数据共享成为了Web应用的一部分,只要是现代浏览器,就可使用。

WebRTC核心组件包括媒体捕获和处理、网络通信、以及点对点数据通道。这些组件使得WebRTC能够支持包括视频通话、语音聊天、文件共享在内的多种实时通信功能。此外,WebRTC 具有极高的安全性,WebRTC使用SRTP(Secure Real-time Transport Protocol)对媒体流进行鉴别。SRTP通过在媒体流上添加数字签名来实现鉴别,以确保通信的隐私性和数据的安全性。

WebRTC 的主要优势在于其实时性和低延迟性,这使得它在视频通话和语音聊天,实时数据传输,直播和流媒体,物联网(IoT),远程医疗等多个领域都有广泛的应用。

WebRTC 的工作原理

WebRTC 的工作原理可以分为三个主要部分:媒体捕获、点对点连接、数据通信。

  1. 媒体捕获: WebRTC 能够从用户的设备(如摄像头、麦克风)中捕获音视频数据,并将这些数据准备好进行传输。这个过程是通过浏览器的 getUserMedia API 实现的,该 API 允许 Web 应用访问用户的多媒体设备,并获取音频或视频流。
  2. 点对点连接: WebRTC 使用点对点(Peer-to-Peer)技术实现设备之间的直接通信。它使用了多种网络协议,如 STUN(Session Traversal Utilities for NAT)、TURN(Traversal Using Relays around NAT)和 ICE(Interactive Connectivity Establishment),以确保两个设备之间可以建立稳定的连接,即使它们在防火墙或 NAT(网络地址转换)之后。
  3. 数据通信: WebRTC 通过数据通道(Data Channel)提供高效的点对点数据传输。数据通道是一种低延迟的通信方式,适用于实时传输数据,如文件、游戏数据、协作文档等。数据通道使用了 SRTP(Secure Real-time Transport Protocol)协议,确保了数据传输的安全性。

WebRTC使用实践

项目的场景是这样的,用户在家安装了摄像头设备,该设备支持 WebRTC,可以捕获视频流。需要实现的功能是:用户可以通过浏览器与摄像头建立 WebRTC 连接,实时查看家中的视频流。

1. 设备端:摄像头设备的 WebRTC 实现

摄像头设备需要实现 WebRTC 的功能,以便与用户的浏览器建立连接。摄像头设备使用 Node.js 和 ws(WebSocket)库来实现信令服务器,允许客户端连接。设备端使用 WebSocket 来接收和发送信令数据。用户通过 WebSocket 与设备进行通信,设备处理信令数据,并利用 WebRTC 建立连接。

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// WebSocket 服务器逻辑
wss.on('connection', (ws) => {
    console.log('Client connected');

    // 当收到来自客户端的信令数据时,转发给摄像头设备
    ws.on('message', (message) => {
        // 将信令数据转发到摄像头设备
        cameraDevice.handleSignalData(message);
    });

    // 当摄像头设备生成信令数据时,发送给客户端
    cameraDevice.setSignalDataHandler((data) => {
        ws.send(data);
    });

    // 当 WebSocket 连接关闭时,可以进行清理操作
    ws.on('close', () => {
        console.log('Client disconnected');
    });
});
// 摄像头设备对象
const cameraDevice = {
    peerConnection: null,

    // 初始化 RTCPeerConnection 对象
    initPeerConnection() {
        this.peerConnection = new RTCPeerConnection();

        // 当 ICE 候选者准备好时,发送给客户端
        this.peerConnection.onicecandidate = ({ candidate }) => {
            if (candidate) {
                this.sendSignalData({ type: 'candidate', candidate });
            }
        };

        // 当接收到远程流时,处理并显示在 video 元素中
        this.peerConnection.ontrack = (event) => {
            const remoteStream = event.streams[0];
            this.onRemoteStream(remoteStream);
        };
    },

    // 处理信令数据
    async handleSignalData(data) {
        if (typeof data === 'string') {
            data = JSON.parse(data); // 解析 JSON 字符串
        }

        switch (data.type) {
            case 'offer':
                await this.handleOffer(data.offer);
                break;
            case 'answer':
                await this.handleAnswer(data.answer);
                break;
            case 'candidate':
                await this.handleCandidate(data.candidate);
                break;
            default:
                console.error('Unknown signal data type:', data.type);
        }
    },

    // 处理 offer 信令数据
    async handleOffer(offer) {
        if (!this.peerConnection) {
            this.initPeerConnection();
        }

        await this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
        const answer = await this.peerConnection.createAnswer();
        await this.peerConnection.setLocalDescription(answer);

        this.sendSignalData({ type: 'answer', answer });
    },

    // 处理 answer 信令数据
    async handleAnswer(answer) {
        if (this.peerConnection) {
            await this.peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
        }
    },

    // 处理 ICE 候选者
    async handleCandidate(candidate) {
        if (this.peerConnection) {
            await this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
        }
    },

    // 发送信令数据到客户端
    sendSignalData(data) {
        if (this.onSignalData) {
            this.onSignalData(JSON.stringify(data)); // 转换为 JSON 字符串
        } else {
            console.error('No handler for sending signal data.');
        }
    },

    // 当远程流可用时调用此函数
    onRemoteStream(remoteStream) {
        // 处理远程流的逻辑,例如显示在视频元素中
        console.log('Received remote stream:', remoteStream);
    },

    // 设置信令数据处理函数
    setSignalDataHandler(handler) {
        this.onSignalData = handler;
    }
};

2. 客户端:浏览器端的 WebRTC 实现

用户通过浏览器与摄像头设备建立 WebRTC 连接,查看实时视频流。用户的浏览器通过 WebSocket 连接到信令服务器,并创建一个 WebRTC 连接 (RTCPeerConnection)。为了使浏览器和摄像头设备建立点对点连接,需要交换一系列信令数据,包括“offer”、“answer”和“ICE候选者”。这些信令数据通过WebSocket信令服务器进行传输。一旦信令数据成功交换,WebRTC 将在两个家里的摄像头设备和浏览器之间建立直接连接。一旦连接建立,摄像头设备的媒体流(包括音频和视频)将通过 WebRTC 进行传输,并显示在客户端浏览器中。用户可以在浏览器中实时看到家中摄像头拍摄的视频。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IoT Camera Viewer</title>
</head>
<body>
    <h1>IoT Camera Viewer</h1>
    <video id="remoteVideo" autoplay playsinline></video>

    <script>
        const remoteVideo = document.getElementById('remoteVideo');
        const socket = new WebSocket('ws://localhost:8080');
        let peerConnection;

        socket.onopen = () => {
            console.log('Connected to signaling server');
            createPeerConnection();
        };

        socket.onmessage = (message) => {
            const signalData = JSON.parse(message.data);
            handleSignalData(signalData);
        };

        function createPeerConnection() {
            peerConnection = new RTCPeerConnection();

            peerConnection.onicecandidate = ({ candidate }) => {
                if (candidate) {
                    socket.send(JSON.stringify({ candidate }));
                }
            };

            peerConnection.ontrack = (event) => {
                // 设置视频源
                remoteVideo.srcObject = event.streams[0];
            };

            // 创建 offer 并发送给摄像头设备
            peerConnection.createOffer().then((offer) => {
                return peerConnection.setLocalDescription(offer);
            }).then(() => {
                socket.send(JSON.stringify({ offer: peerConnection.localDescription }));
            });
        }

        function handleSignalData(signalData) {
            if (signalData.answer) {
                peerConnection.setRemoteDescription(new RTCSessionDescription(signalData.answer));
            } else if (signalData.candidate) {
                peerConnection.addIceCandidate(new RTCIceCandidate(signalData.candidate));
            }
        }
    </script>
</body>
</html>

最后

经过一番调试,看到了视频画面,传输很流畅,总算没白忙活。WebRTC在实时音视频传输方面果然给力,延时很低。功能虽然实现了,但我发现WebRTC的周边概念和属性其实还蛮多的,需要再深入学习了解一下,无论学什么,不能只满足于用到哪些只学哪些知识点,学得越全面,越深刻,才能少踩很多坑,交付高质量产品。