初识WebRTC

331 阅读2分钟
作者:CQF

简单写一些关于WebRTC方面的科普文章,一眼能够扫完

什么是WebRTC?

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。

MDN 上对 WebRTC 的描述,浏览器点对点之间的连接,不是说不需要中间媒介,难道是需要服务器吗?
通过下面架构图可以看出,在浏览器建立点对点连接之前,需要借助WebSocket创建持久化连接,WebSocket作为双方信令服务器。 这里的信令服务器,用于转发信息。而 WebRTC 不借助中间媒介 的意思是,在建立对等连接后,不需要借助第三方服务器中转,而是直接在两个实体(浏览器)间进行传输。

名称解释

  • ICE:交互式连接设施Interactive Connectivity Establishment (ICE) 是一个允许你的浏览器和对端浏览器建立连接的协议框架
  • SDP:会话描述协议Session Description Protocol (SDP) 是一个描述多媒体连接内容的协议
  • offer:提议创建方
  • answer:接受方
  • RTCPeerConnection:WebRTC点对点通信的API
  • RTCDataChannel:WebRTC实现两个点之间发送和接收任意数据的API(不是本例使用api) image.png
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title></title>
    <style>
        * {
            padding: 0;
            margin: 0;
            box-sizing: border-box;
        }
        .container {
            width: 100%;
            display: flex;
            display: -webkit-flex;
            justify-content: space-around;
            padding-top: 20px;
        }
        .video-box {
            position: relative;
            width: 800px;
            height: 400px;
        }
        #remote-video {
            width: 100%;
            height: 100%;
            display: block;
            object-fit: cover;
            border: 1px solid #eee;
            background-color: #F2F6FC;
        }
        #local-video {
            position: absolute;
            right: 0;
            bottom: 0;
            width: 240px;
            height: 120px;
            object-fit: cover;
            border: 1px solid #eee;
            background-color: #EBEEF5;
        }
        .start-button {
            position: absolute;
            left: 50%;
            top: 50%;
            width: 100px;
            display: none;
            line-height: 40px;
            outline: none;
            color: #fff;
            background-color: #409eff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transform: translate(-50%, -50%);
        }
        .logger {
            width: 40%;
            padding: 14px;
            line-height: 1.5;
            color: #4fbf40;
            border-radius: 6px;
            background-color: #272727;
        }
        .logger .error {
            color: #DD4A68;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="video-box">
            <video id="remote-video"></video>
            <video id="local-video" muted></video>
            <button class="start-button" onclick="startLive()">start</button>
        </div>
        <div class="logger"></div>
    </div>
    <script>
        const message = {
            el: document.querySelector('.logger'),
            log (msg) {
                this.el.innerHTML += `<span>${new Date().toLocaleTimeString()}${msg}</span><br/>`;
            },
            error (msg) {
                this.el.innerHTML += `<span class="error">${new Date().toLocaleTimeString()}${msg}</span><br/>`;
            }
        };
         
        const target = location.search.slice(6);
        const localVideo = document.querySelector('#local-video');
        const remoteVideo = document.querySelector('#remote-video');
        const button = document.querySelector('.start-button');
 
        localVideo.onloadeddata = () => {
            message.log('播放本地视频');
            localVideo.play();
        }
        remoteVideo.onloadeddata = () => {
            message.log('播放对方视频');
            remoteVideo.play();
        }
 
        document.title = target === 'offer' ? '发起方' : '接收方';
 
        message.log('信令通道(WebSocket)创建中......');
        // 本地
        // const socket = new WebSocket('ws://localhost:9999');
        const socket = new WebSocket('wss://ws2.dev.yingzi.com');
        socket.onopen = () => {
            message.log('信令通道创建成功!');
            target === 'offer' && (button.style.display = 'block');
        }
        socket.onerror = () => message.error('信令通道创建失败!');
        socket.onmessage = e => {
            const { type, sdp, iceCandidate } = JSON.parse(e.data)
            if (type === 'answer') {
                peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));
            } else if (type === 'answer_ice') {
                peer.addIceCandidate(iceCandidate);
            } else if (type === 'offer') {
                startLive(new RTCSessionDescription({ type, sdp }));
            } else if (type === 'offer_ice') {
                peer.addIceCandidate(iceCandidate);
            }
        };
 
        const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
        !PeerConnection && message.error('浏览器不支持WebRTC!');
        const peer = new PeerConnection();
 
        peer.ontrack = e => {
            if (e && e.streams) {
                message.log('收到对方音频/视频流数据...');
                remoteVideo.srcObject = e.streams[0];
            }
        };
 
        peer.onicecandidate = e => {
            if (e.candidate) {
                message.log('搜集并发送候选人');
                socket.send(JSON.stringify({
                    type: `${target}_ice`,
                    iceCandidate: e.candidate
                }));
            } else {
                message.log('候选人收集完成!');
            }
        };
 
        async function startLive (offerSdp) {
            target === 'offer' && (button.style.display = 'none');
            let stream;
            try {
                message.log('尝试调取本地摄像头/麦克风');
                stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
                message.log('摄像头/麦克风获取成功!');
                localVideo.srcObject = stream;
            } catch {
                message.error('摄像头/麦克风获取失败!');
                return;
            }
 
            message.log(`------ WebRTC ${target === 'offer' ? '发起方' : '接收方'}流程开始 ------`);
            message.log('将媒体轨道添加到轨道集');
            stream.getTracks().forEach(track => {
                peer.addTrack(track, stream);
            });
 
            if (!offerSdp) {
                message.log('创建本地SDP');
                const offer = await peer.createOffer();
                await peer.setLocalDescription(offer);
                 
                message.log(`传输发起方本地SDP`);
                socket.send(JSON.stringify(offer));
            } else {
                message.log('接收到发送方SDP');
                await peer.setRemoteDescription(offerSdp);
 
                message.log('创建接收方(应答)SDP');
                const answer = await peer.createAnswer();
                message.log(`传输接收方(应答)SDP`);
                socket.send(JSON.stringify(answer));
                await peer.setLocalDescription(answer);
            }
        }
    </script>
</body>
</html>

针对于,Websocket需要注意,除了本地调试的localhost

建立通信必须要使用域名,不能使用ip或者域名加端口 建立通信必须要使用域名,不能使用ip或者域名加端口 建立通信必须要使用域名,不能使用ip或者域名加端口

另外,使用navigator.mediaDevices.getUserMedia调用本地多媒体设备(不局限于摄像头)时,存在一个安全问题,为了用户的隐私安全,必须使用https,对应WebSocket协议应该换成https。

One more thing

在撰写本文的时候,《火山引擎、阿里云、腾讯云联合发布“超低延时”直播技术标准 延时最快500毫秒》一文引起笔者的注意,有大厂技术加持和实践使用场景,未来WebRTC会跟多落地场景。