WebRTC初学Demo


详细请访问

简介

WebRTC(网页实时通信技术),是一系列为了建立端到端文本或者随机数据的规范,标准,API和概念的统称。

任何实现了WebRTC标准的软件之间均可通信,如PC浏览器–手机浏览器、浏览器–App、App–App。通信双方是对等的,但通常还要引入服务端,以便于对等端能够找到对方。

移动端浏览器对WebRTC的支持

Android 4.4以上
iOS 11以上
WebRTC应用不需要非常高性能就能够平稳运行、拥有良好体验。对于WebRTC来说,获取特定硬件的权限也不是必需的。Web应用和原生应用对比:

原生应用会比Web应用更快,拥有更高级别的硬件权限;混合应用的运行速度更慢,不能使用移动设备的全部权限。
原生应用在不同的平台上的代码复用度有待提高;混合应用开发起来会更快,更省钱
结论:如果高性能和极致体验不是软件的必不可少的要求,那么,在绝大多数情况下,用户根本注意不到原生和混合WebRTC应用之间的差别。

实现基本的数据通道
对浏览器而言,WebRTC API处于底层且十分复杂。不过我们可以使用封装好的高级别的API----simple-peer是一个基础的,非常洁净的低层P2P连接封装器.下面的示例是simple-peer的一个标准例子。

<html>
  <body>
      <style>
        #outgoing {
          width: 100%;
          word-wrap: break-word;
          white-space: normal;
        }
      </style>
    <form>
      <textarea id="incoming"></textarea>
      <button type="submit">submit</button>
    </form>
    <pre id="outgoing"></pre>
    <script src="simplepeer.min.js"></script>
    <script>
      const p = new SimplePeer({
        initiator: location.hash === '#1',
        trickle: false
      })

      p.on('error', err => console.log('error', err))

      p.on('signal', data => {
        console.log('SIGNAL', JSON.stringify(data))
        document.querySelector('#outgoing').textContent = JSON.stringify(data)
      })

      document.querySelector('form').addEventListener('submit', ev => {
        ev.preventDefault()
        p.signal(JSON.parse(document.querySelector('#incoming').value))
      })

      p.on('connect', () => {
        console.log('CONNECT')
        p.send('whatever' + Math.random())
      })

      p.on('data', data => {
        console.log('data: ' + data)
      })
    </script>
  </body>
</html>
View Code

 演示步骤:

 

1、创建index.html文件,文件内容为上面的代码

2、下载simplepeer.min.js,放到和index.html同目录下

3、用浏览器打开file:///本地路径目录/index.html#1(这里使用file协议,是因为没有部署index.html到服务端),可以看到网页中输出了一个offer邀请信令,格式类似下面例子

{"type":"offer","sdp":"v=0\r\no=- 8456412157049919494 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\nm=application 53799 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 171.84.6.216\r\na=candidate:1841888059 1 udp 2113937151 0ec970aa-e465-4105-a65e-81ec6552c7e5.local 53799 typ host generation 0 network-cost 999\r\na=candidate:842163049 1 udp 1677729535 171.84.6.216 53799 typ srflx raddr 0.0.0.0 rport 0 generation 0 network-cost 999\r\na=ice-ufrag:UqLe\r\na=ice-pwd:f08ur1ltpmFSGbMp/cwf9KkF\r\na=fingerprint:sha-256 71:60:B4:28:15:B6:5E:D4:CE:76:49:F8:B2:CE:44:E4:F8:F9:FD:14:7B:AA:DA:AA:BE:DF:66:62:27:2E:E9:00\r\na=setup:actpass\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n"}

1

4、用另外一个浏览器打开file:///本地路径目录/index.html,注意没有#1,然后将步骤3中的offer信令复制到本步骤中网页的提交框中,并提交,然后可以在网页中看到一answer应答信令,格式类似下面例子

{"type":"answer","sdp":"v=0\r\no=- 5040306846154650858 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=candidate:1841888059 1 udp 2113937151 71a8e768-d70d-4554-8ce3-c3d1ee6751a0.local 49173 typ host generation 0 network-cost 999\r\na=ice-ufrag:VhmK\r\na=ice-pwd:KVNGXnCYlO1TaAqAEnhObWP4\r\na=fingerprint:sha-256 5E:09:41:CF:D8:25:46:D4:E4:D7:B6:FB:6D:E7:18:7F:BE:CB:4B:98:28:72:95:06:78:24:FB:6C:6F:B4:50:AE\r\na=setup:active\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n"}

1

5、将步骤4中的answer信令制到步骤3中网页的提交框中,并提交。打开浏览器的开发者模式,可以看到两个浏览器已经建立了数据通道,并互相发送了一个随机数消息

————————————————

文件传输

建立起基本的数据通道后,就可以做一些比较直观的不那么抽象事情了,比如传输文件。对于使用WebRTC进行文件传输,有一些细节需要处理

修改传输速度限制

Chrome默认将WebRTC的数据通道传输速度限制在30K,我们可以修改SDP的内容来提高上限,例如256K。

let peer = new SimplePeer()
peer.on('signal', data => {
    data.sdp = data.sdp.replace( 'b=AS:30', 'b=AS:262144' );
    console.log('SIGNAL', JSON.stringify(data))
})
View Code

选择并读取文件数据

我们选取基本的input控件作为文件选择器,并监听文件选择动作。//考虑到传输速度限制,在读取文件内容时,应进行切片–循环读取、发送,直到文件末尾。

<input type="file" onchange="OnFileSelected(this.files)"/>
<script>
    //选中文件
    function OnFileSelected(files)
    {
        file = files[0];
        let fileReader = new FileReader();
        fileReader.onload = (ev)=>{
            let blobData = ev.currentTarget.result;
            console.log(blobData)
        }
        fileReader.readAsArrayBuffer( file.slice( 0, file.size ) );
    }
</script>
View Code

自动下载文件

接收完成后,数据还保存在浏览器内存(缓存)中,需要触发文件下载机制将数据下载到本地。

let blobData = '接收的数据'
let blob = new window.Blob( blobData );
let anchor = document.createElement( 'a' );
anchor.href = URL.createObjectURL( blob );
anchor.download = '文件名';
anchor.textContent = '下载文件';
anchor.click()
View Code

完整代码

<html>
    <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width,initial-scale=1"/>
    </head>
  <body>
    <style>
        #outgoing {
          width: 100%;
          word-wrap: break-word;
          white-space: normal;
        }
      </style>
    <form>
      <textarea id="incoming"></textarea>
      <button type="submit">提交</button>
    </form>
    <div>口令</div>
    <pre id="outgoing"></pre>
    
    <input id="fileinput" type="file" onchange="OnFileSelected(this.files)" hidden="true"/>
    <script src="simplepeer.min.js"></script>
    <script>
        //单个块大小
        const MAX_CHUNK_SIZE = 1024 * 256;

        //选中文件
        function OnFileSelected(files)
        {
            file = files[0];
            sendFile(file)
        }

        var peer;
        if (location.hash === '#1') {
            peer = new SimplePeer({ initiator: true,
                                    trickle: false});
        } else {
            peer = new SimplePeer({ initiator: false,
                                    trickle: false});
        }
        peer.on('error', err => console.log('error', err));
        peer.on('error', err => console.log('error', err));
        peer.on('signal', data => {
            //modify sdp to increase block size
            data.sdp = data.sdp.replace( 'b=AS:30', 'b=AS:'+MAX_CHUNK_SIZE.toString() ); 
            document.querySelector('#outgoing').textContent = JSON.stringify(data);
        });

        document.querySelector('form').addEventListener('submit', ev => {
            ev.preventDefault()
            peer.signal(JSON.parse(document.querySelector('#incoming').value));
        });

        peer.on('connect', () => {
            console.log('CONNECT');
            // show file input button
            document.querySelector('#fileinput').removeAttribute("hidden")
        });
        
        // receive file
        peer.on('data', data => {
            recvFile(data);
        });

        function sendFile(file)
        {
            console.log("send file name size");
            peer.send(JSON.stringify({
                fileName: file.name,
                fileSize: file.size
            }));

            console.log("send file data");
            let fileReader = new FileReader();
            let readEnd = {"end":false};
            let startPos = 0;

            function readFile(file)
            {
                let end = startPos + MAX_CHUNK_SIZE;
                if (end > file.size) {
                    end = file.size;
                    readEnd.end = true;
                }
                let fc = file.slice( startPos, end );
                startPos = end;
                fileReader.readAsArrayBuffer( fc );
            }
            
            fileReader.onload = (ev)=>{
                let blobData = ev.currentTarget.result;
                console.log("send file data ", startPos)
                peer.send(blobData);
                if (readEnd.end === false) {
                    readFile(file)
                }
            }

            readFile(file);
        }

        let recvFileInfo = undefined;
        let recvFileData = [];
        let recvLen = 0;
        function recvFile(data)
        {
            if (recvFileInfo === undefined)
            {
                recvFileInfo = JSON.parse( data.toString() );
            }
            else 
            {
                recvLen += data.byteLength;
                recvFileData.push(data);
                console.log("recv " + recvFileInfo.fileName + " " + recvLen.toString() + "/" + recvFileInfo.fileSize.toString());
                // receive end
                if (recvLen >= recvFileInfo.fileSize)
                {
                    let blob = new window.Blob( recvFileData );
                    var anchor = document.createElement( 'a' );
                    anchor.href = URL.createObjectURL( blob );
                    anchor.download = recvFileInfo.fileName;
                    anchor.textContent = '下载';
                    recvFileInfo = undefined;
                    anchor.click();
                }
            }
        }
    </script>
  </body>
</html>
View Code

演示步骤:

1、创建index.html文件,文件内容为上面的代码
2、下载simplepeer.min.js,放到和index.html同目录下
3、用浏览器打开file:///本地路径目录/index.html#1(这里使用file协议,是因为没有部署index.html到服务端),可以看到网页中输出了一个offer邀请信令。
4、用另外一个浏览器打开file:///本地路径目录/index.html,注意没有#1,然后将步骤3中的offer信令复制到本步骤中网页的提交框中,并提交,然后可以在网页中看到一answer应答信令。
5、将步骤4中的answer信令制到步骤3中网页的提交框中,并提交。打开浏览器的开发者模式,可以看到两个浏览器已经建立了数据通道,此时可以传输文件了。
————————————————

音视频通话

还是使用simple-peer,利用浏览器的媒体能力,搭建一个简单的局域网内音视频通话模型。

https

一般情况下,在线网页使用浏览器的摄像头和麦克风能力时,浏览器的要求是使用htpps。这里,我们使用python简单的搭建一个https

访问音视频流

我们可以使用navigator.mediaDevices.getUserMedia来获取音视频流数据

navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        }).then((stream)=>{
            //音视频流 stream
        }).catch(() => {
            //捕获到异常
        })
View Code

获取到媒体流后,可以将媒体数据展示在video标签上,这样更加直观。
需要注意的是,对于iOS设备,建议使用iOS11以后的版本,且需要添加playsinline webkit-playsinline="true"以解决video标签的黑屏/白屏问题。

<video id="idvv" playsinline webkit-playsinline="true" />
<script>
    navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        }).then((stream)=>{
            //音视频流 stream
            var video = document.querySelector("idvv")
            if ('srcObject' in video) {
                video.srcObject = stream
            } else {
                video.src = window.URL.createObjectURL(stream)
            }
            video.play()
        }).catch(() => {
            //捕获到异常
        })
</script>
View Code

完整代码

<html>
    <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width,initial-scale=1"/>
    </head>
  <body>
    <style>
        #outgoing {
          width: 100%;
          word-wrap: break-word;
          white-space: normal;
        }
      </style>
    <form>
      <textarea id="incoming"></textarea>
      <button type="submit">提交</button>
    </form>
    <div>口令</div>
    <pre id="outgoing"></pre>
    <div>媒体流</div>

    <!-- ios 11以上支持video,同时需要 添加 playsinline webkit-playsinline="true" ,解决黑屏/白屏幕问题  -->
    <video width="40%" id="streamLocal" playsinline webkit-playsinline="true" />
    <video width="40%" id="streamRemote" playsinline webkit-playsinline="true" />

    <script src="simplepeer.min.js"></script>
    <script>
        // get video/voice stream
        navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        }).then(gotMedia).catch(() => {})
      
        var peer;
        function gotMedia (stream) {
            // show local record video stream 
            showVideoStream("#streamLocal",stream)

            if (location.hash === '#1') {
                peer = new SimplePeer({ initiator: true,
                                        trickle: false,
                                        stream: stream })
            } else {
                peer = new SimplePeer({initiator:false,
                                       trickle: false,
                                       stream: stream})
            }
            peer.on('error', err => console.log('error', err))

            peer.on('signal', data => {
                console.log('SIGNAL', JSON.stringify(data))
                document.querySelector('#outgoing').textContent = JSON.stringify(data)
            })

            document.querySelector('form').addEventListener('submit', ev => {
                ev.preventDefault()
                peer.signal(JSON.parse(document.querySelector('#incoming').value))
            })

            peer.on('stream', stream => {
                // got remote video stream, now let's show it in a video tag
                showVideoStream("#streamRemote",stream)
            })
        }

        function showVideoStream(id,stream) {
            var video = document.querySelector(id)
            if ('srcObject' in video) {
                video.srcObject = stream
            } else {
                video.src = window.URL.createObjectURL(stream) // for older browsers
            }
            video.play()
        }
    </script>
  </body>
</html>
View Code

演示步骤:

1、创建index.html文件,文件内容为上面的代码,放在https_server.py同目录下
2、下载simplepeer.min.js,放到和index.html同目录下
3、运行 python https_server.py
4、用浏览器打开https://ip地址:4443/index.html#1,可以看到网页中输出了一个offer邀请信令。
5、用局域网内的另外一个终端浏览器打开https://ip地址:4443/index.html,注意没有#1,然后将步骤4中的offer信令复制到本步骤中网页的提交框中,并提交,然后可以在网页中看到一answer应答信令。
6、将步骤4中的answer信令制到步骤3中网页的提交框中,并提交。顺利的话,可以两个浏览器可以显示到对方的媒体流
————————————————

屏幕共享

屏幕共享的原理和音视频通话的原理类似,唯一不同的是媒体流的数据来源不一样,因此,我们可以在音视频通话的基础上,修改一个屏幕共享的实现。

//Safari浏览器对MediaStream支持不够友好,建议使用两个Chrome浏览器测试。

捕捉屏幕

Chrome浏览器在2018年后才开始支持navigator.mediaDevices.getDisplayMedia

navigator.mediaDevices.getDisplayMedia({
                video: true,
                audio: true
            }).then( (stream)=>{
                // stream 屏幕数据
            }).catch(() => {
                (e) => {console.log(e)}
            })
View Code

完整代码

<html>
    <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width,initial-scale=1"/>
    </head>
  <body>
    <style>
        #outgoing {
          width: 100%;
          word-wrap: break-word;
          white-space: normal;
        }
      </style>
    <form>
      <textarea id="incoming"></textarea>
      <button type="submit">提交</button>
    </form>
    <div>口令</div>
    <pre id="outgoing"></pre>
    <!-- ios 11以上支持video,同时需要 添加 playsinline webkit-playsinline="true" ,解决黑屏/白屏幕问题  -->
    <video id="screenShare" playsinline webkit-playsinline="true"></video>

    <script src="simplepeer.min.js"></script>
    <script>
        // get video/voice stream


        if (location.hash === '#1') {
            navigator.mediaDevices.getDisplayMedia({
                video: true,
                audio: true
            }).then( gotMedia).catch(() => {(e) => {console.log(e)}})
        } else {
            gotMedia(null)
        }
        var peer;
        function gotMedia (stream) {
            if (stream !== null) {
                peer = new SimplePeer({ initiator: true,
                                        trickle: false,
                                        stream: stream })
            } else {
                peer = new SimplePeer({initiator:false,
                                       trickle: false})
            }
            peer.on('error', err => console.log('error', err))

            peer.on('signal', data => {
                console.log('SIGNAL', JSON.stringify(data))
                document.querySelector('#outgoing').textContent = JSON.stringify(data)
            })

            document.querySelector('form').addEventListener('submit', ev => {
                ev.preventDefault()
                peer.signal(JSON.parse(document.querySelector('#incoming').value))
            })

            peer.on('stream', stream => {
                // got remote video stream, now let's show it in a video tag
                showVideoStream("#screenShare",stream)
            })
        }

        function showVideoStream(id,stream) {
            var video = document.querySelector(id)
            if ('srcObject' in video) {
                video.srcObject = stream
            } else {
                video.src = window.URL.createObjectURL(stream) // for older browsers
            }
            video.play()
        }
    </script>
  </body>
</html>
View Code

演示步骤和音视频通话的例子基本一样。

 

NAT穿透之STUN/TURN
NAT(Network Address Translation,网络地址转换),典型的应用场景是公网IP地址和私有IP地址转换,通过使用少量的公网IP地址来代表较多的私有IP地址的方式,将有助于减缓可用的IP地址空间的枯竭。

NAT分为四种类型,Full Cone(完全锥形)、Restricted Cone(限制锥形)、Port Restricted Cone(端口限制锥形)和Symmetric(对称形)。

为了能让两个处在不同NAT网络的WebRTC终端能够进行数据、媒体通信,需要借助STUN/TURN来实现NAT穿透。

一般来说,STUN(Simple Traversal of User Datagram Protocol Through Network Address Translators), NAT的UDP的简单穿越,是一种网络协议,通过暴露两个WebRTC终端在公网上的地址,可以在锥形NAT(完全锥形、限制锥形、端口限制锥形)进行穿透,从而实现P2P通信。

TURN(Traversal Using Relays around NAT),也是一种网络协议,可以解决STUN不能穿透对称形NAT的问题。和STUN不同的是,TURN实现穿透的方法是中转。

如果在公网上架设了STUN/TURN服务器,那么处在不同NAT网络的WebRTC终端进行通信就变得比较容易了。在初始化WebRTC终端时,可以配置STUN/TURN服务地址列表:

peer = new SimplePeer({ config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:global.stun.twilio.com:3478?transport=udp' }] }, })

//ICE,与STUN和TURN相比,ICE并非是解决NAT穿透问题的协议,而是一个框架,整合其他现存的NAT穿透协议,如STUN、TURN、RSIP等。

使用STUN/TURN后,可以较为方便的实现NAT穿透。当然,STUN/TURN不是必需的,但是强烈推荐使用。

 

信令服务器
为何还需要引入信令服务器?先回顾下本文上面的例子,使用simple-peer创建WebRTC终端,生成SDP信令(如offer、answer),那么他们如何交换信令呢?本文上文的例子是通过手动复制粘贴实现的,而真实的业务场景中,这个人工操作是由信令服务器完成的。

WebRTC并没有规定信令服务使用哪种协议实现,所以本文采用最简单的实现方式(真实业务场景不会使用),HTTP + GET + POST实现。
先来熟悉下引入信令服务器后,两个WebRTC终端建立连接的过程:

1、A端 初始化offer信令.
2、A端 向STUN/TURN查询自己的公网地址,STUN/TURN会返回candidate列表A(可连接的候选地址列表).
3、A端 将offer信令和candidate列表A一同发给信令务器,由信令服务器转发给 B端.
4、B端 收到offer信令和candidate列表A, 从candidate列表A选择合适的地址发送NAT穿透报文, 同时向STUN/TURN查询自己的公网地址获取candidate列表B.
5、B端 生成answer信令,并将answer信令和candidate列表B发送给信令服务器,由信令服务器转发给 A端.
6、A端 收到answer信令和candidate列表B,从candidate列表B选择合适的地址发送NAT穿透报文.
7、A、B建立连接。
//以上只是基本的连接建立过程,当然真实业务场景会比较复杂,如可能有媒体协商。真实的业务场景下,连接过程依然可以从上面的基本过程演变出来。

信令服务实现

上面说到本文使用HTTP + GET + POST实现,客户端使用POST负责发送信令,通过GET轮询获取信令。
依然适用Python实现。

import sys

if sys.version_info.major > 2:   # python 3
    from http.server import HTTPServer as BaseHTTPServer
    from http.server import BaseHTTPRequestHandler as SimpleHTTPRequestHandler
else: #python 2
    from BaseHTTPServer import HTTPServer
    from SimpleHTTPServer import SimpleHTTPRequestHandler

import ssl

WEBRTC_OFFER = None
WEBRTC_ANSWER = None

class  WebRtcSignalHandler (SimpleHTTPRequestHandler):
    def do_GET(self):
        if (self.path == "/webrtc_offer"):
            self.do_http_response_with(200,WEBRTC_OFFER)
        elif (self.path == "/webrtc_answer"):
            self.do_http_response_with(200,WEBRTC_ANSWER)
        else:
            SimpleHTTPRequestHandler.do_GET(self)
    
    def do_POST(self):
        length = int(self.headers.getheader('content-length'))
        body = self.rfile.read(length)
        if (self.path == "/webrtc_offer"):
           global WEBRTC_OFFER
           global WEBRTC_ANSWER
           WEBRTC_OFFER = body
           WEBRTC_ANSWER = None
           self.do_http_response_with(200,"Success")
        elif (self.path == "/webrtc_answer"):
           global WEBRTC_ANSWER
           WEBRTC_ANSWER = body
           self.do_http_response_with(200,"Success")
        else:
            SimpleHTTPRequestHandler.do_POST(self)

    def do_http_response_with(self,code,msg):
        self.send_response(code)
        self.end_headers()
        if (msg):
            if not self.wfile.closed:
                self.wfile.write(msg)
                # self.wfile.flush()


httpd = HTTPServer(('', 4443),WebRtcSignalHandler)

httpd.socket = ssl.wrap_socket (httpd.socket,
        keyfile="key.pem", # 密码123456
        certfile='cert.pem', server_side=True)

httpd.serve_forever()
View Code

完整代码

<html>
    <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width,initial-scale=1"/>
    </head>
  <body>
    <div>媒体流</div>

    <!-- ios 11以上支持video,同时需要 添加 playsinline webkit-playsinline="true" ,解决黑屏/白屏幕问题  -->
    <video width="40%" id="streamLocal" playsinline webkit-playsinline="true"></video>
    <video width="40%" id="streamRemote" playsinline webkit-playsinline="true"></video>

    <script src="simplepeer.min.js"></script>
    <script>
        // get video/voice stream
        navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        }).then(gotMedia).catch(() => {})
      
        var peer
        var candidateList = []
        var sdp = undefined
        const initiator = (location.hash === '#1')
        function gotMedia (stream)
        {
            // show local record video stream 
            showVideoStream("#streamLocal",stream)

            // ice config
            let iceConfig = [{ urls: 'stun:stun1.l.google.com:19302' },
                             /*{ urls: 'stun:stun2.l.google.com:19302' },
                             { urls: 'stun:stun3.l.google.com:19302' },
        { urls: 'stun:stun4.l.google.com:19302' },*/]
            
            peer = new SimplePeer({ initiator: initiator,
                                    stream: stream ,
                                    config:{iceServers : iceConfig}})
  
            peer.on('error', err => console.log('error', err))

            peer.on('signal', data => {
                if (data.type === "candidate") {
                    candidateList.push(data);
                }  else if (data.type === "offer" || data.type === "answer")  {
                    sdp = data;
                    // 获取sdp信息 ,2秒后将 sdp 和 candidate 发给 信令服务器
                    setTimeout(() => {
                        msg = {sdp:sdp , candidate:candidateList}
                        sendMsgSdpCandidate(msg);
                        console.log(JSON.stringify(msg))
                    }, 2000)
                }
            })

            setTimeout(() => {
                // 获取 sdp
                fetchMsgSdpCandidate((msg)=>{
                    if ((msg.sdp.type === "answer" && initiator ===true)
                     || (msg.sdp.type === "offer" && initiator ===false))
                    {
                        if (msg.candidate !== undefined ) {
                            msg.candidate.forEach( (c)=>{
                                peer.signal(c)
                            })
                        }
                        peer.signal(msg.sdp)
                    }
                })
            }, 2000);

            peer.on('stream', stream => {
                // got remote video stream, now let's show it in a video tag
                showVideoStream("#streamRemote",stream)
            })
        }

        function sendMsgSdpCandidate(msg)
        {
            let xhr = new XMLHttpRequest()
            xhr.open("POST", initiator ? "webrtc_offer" : "webrtc_answer", true)
            xhr.send(JSON.stringify(msg))
        }

        function fetchMsgSdpCandidate( callback )
        {
            // 为节省服务编码,此处使用http get轮询方式。
            // 正常业务中,可以使用websocket,避免轮询冲击。
            let xhr = new XMLHttpRequest()
            xhr.onload = function() {
                if (xhr.status == 200 && xhr.responseText) {
                    msg = JSON.parse(xhr.responseText)
                    callback(msg)
                }
                else {
                    setTimeout(() => {
                        fetchMsgSdpCandidate(callback)
                    }, 2000);
                }
            }
            xhr.open("GET", initiator ? "webrtc_answer" : "webrtc_offer")
            xhr.send()
        }

        function showVideoStream(id,stream)
        {
            var video = document.querySelector(id)
            if ('srcObject' in video) {
                video.srcObject = stream
            } else {
                video.src = window.URL.createObjectURL(stream) // for older browsers
            }
            video.play()
        }
    </script>
  </body>
</html>
View Code

演示步骤:

1、创建index.html文件,文件内容为上面的代码
2、下载simplepeer.min.js,放到和index.html同目录下
3、用浏览器打开https://公网地址/index.html#1。
4、用另外一个浏览器打开https:///公网地址/index.html,注意没有#1。
5、正常情况下,应能建立连接。
————————————————
原文链接:https://blog.csdn.net/holdsky/article/details/120841013

https://github.com/feross/simple-peer

 

posted @ 2023-06-12 14:19  笠航  阅读(487)  评论(0编辑  收藏  举报