上一篇我们跑通了一个简单的WebRTCDemo,相当于WebRTC中的HelloWorld。在同一个页面中进行WebRTC通信,比较难看出效果,这次我们再进一步,进行一个局域网内的单向通信。一个页面推流,一个页面拉流。

效果

在这里插入图片描述

流程图

在这里插入图片描述

相关术语

offer

offer可以理解是一个PeerConnection的能力列表,是建立连接的两个PeerConnection中的发起端。Demo中都是从推流端开始发起(其实也可以从订阅端开始发起)。

offer中的内容大概描述的情况

● 一条视频流
○ 发送
○ 视频编码能力

answer

answer也是一个PeerConnection的能力列表,是建立连接的两个PeerConnection中的接收端(这里的接收端不是视频的接收端,是建立连接的接收端)。

answer中的内容大概描述的情况

● 一条视频流
○ 接收
○ 视频解码能力

icecandidate

icecandidate是PeerConnection返回的消息。其中包含了当前机器的ip地址、可用端口的相关信息。需要把icecandidate发送给另一个PeerConnection,用于两个PeerConnection建立连接。

MediaStream

MediaStream是一个媒体流的概念,里面可以有音频流、视频流两个类型的流。但是不仅限于一个音频流和一个视频流,可以有多个不同的音频流+视频流。Demo中只包含了一个视频流,是为了尽量简化Demo,方便理解。

协商

两个PeerConnection交换offer和answer的过程就叫做协商。是两个PeerConnection协商能力的过程,以Demo为例,如果协商成功,则表示两个PeerConnection可以建立连接。如果失败则表示不能建立连接。

举个例子

  1. 如果offer中只有视频发送的能力,answer中也只有视频发送的能力,则表示两个PeerConnection不能建立连接。
  2. 如果offer中只有视频发送的能力,answer中只有视频接收的能力。
    ○ 如果发送的编码能力只有H264,但是接收的解码能力只有VP8,协商也会失败。
    ○ 如果发送的编码能力和接收端的解码能力有交集,则可以建立连接

代码

以下是源码,也可以通过gitee直接获取代码

服务端

要注意server依赖了nodejs-websocket模块。

var ws = require("nodejs-websocket");


var pub_ws = null;
var sub_ws = null;


function start() {
  var msg = JSON.stringify({ type: "start" });
  pub_ws.send(msg);
}

var server = ws.createServer(function (conn) {
  // 收到websocket连接
  conn.on("text", function (str) {
    if (pub_ws === conn) {
      if (sub_ws) {
        sub_ws.send(str);
      }
    } else if (sub_ws === conn) {
      if (pub_ws) {
        pub_ws.send(str);
      }
    } else {
      let obj = JSON.parse(str);
      if (obj.type === 'publish') {
        pub_ws = conn;
        if (sub_ws) {
          start();
        }
      } else if (obj.type === 'subscribe') {
        sub_ws = conn;
        if (pub_ws) {
          start();
        }
      }
    }
  })

  conn.on("error", function (event) {

  });

  conn.on("close", function (code, reason) {
    if (conn === pub_ws) {
      console.log("remove pub")
      pub_ws = null;
    } else if (conn === sub_ws) {
      console.log("remove sub")
      sub_ws = null;
    }
  })
}).listen(9000);
推流端

推流页面需要用localhost访问,因为获取设备需要https或者localhost(127.0.0.1)才可以。获取设备部分可以参考获取浏览器麦克风、摄像头和屏幕共享
其中websoket server的ip写的是127.0.0.1,可以自行改成websocket server的局域网ip地址。

<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>推流页面</title>
</head>

<body>
  <video id="localStream" style="width: 320px; height: 240px;" autoplay muted></video>
  <script>
    // 获取摄像头返回的MediaStream
    let localStream = null;

    // 显示本地画面的VideoElement
    let localVideo = document.getElementById("localStream");

    // 建立连接按钮
    let startBtn = document.getElementById("startBtn");

    // 推流用的MediaStream
    let pc_pub = new RTCPeerConnection();

    let ws = new WebSocket('ws://127.0.0.1:9000');
    ws.addEventListener('open', () => {
      // 通知server pub已经上线
      ws.send(JSON.stringify({
        type: "publish"
      }))
    })

    ws.addEventListener('message', (event) => {
      let msg = JSON.parse(event.data);
      switch (msg.type) {
        case "start":
          start();
          break;

        case "answer":
          pc_pub.setRemoteDescription(msg).then(() => {

          }).catch((err) => {

          })
          break;

        default:
          pc_pub.addIceCandidate(msg);
          break;
      }
    })

    pc_pub.addEventListener('icecandidate', (event) => {
      if (event.candidate) {
        ws.send(JSON.stringify(event.candidate));
      }
    })

    function start() {
      getDevice().then((mediaStream) => {
        pc_pub.addTrack(mediaStream.getVideoTracks()[0], mediaStream);
        pc_pub.createOffer().then((offer) => {
          pc_pub.setLocalDescription(offer).then(() => {
            ws.send(JSON.stringify(offer));
          }).catch((err) => {
            console.error('setLocalDescription error', err);
          })
        }).catch((err) => {
          console.error("create offer error", err);
        })
      }).catch((err) => {
        console.error("getDevice error", err);
      })
    }

    function getDevice() {
      return new Promise((resolve, reject) => {
        navigator.mediaDevices.getUserMedia({ video: true }).then((mediaStream) => {
          localVideo.srcObject = mediaStream;
          resolve(mediaStream);
        }).catch((err) => {
          reject(err);
        })
      })
    }
  </script>
</body>

</html>
推流端

其中websoket server的ip写的是127.0.0.1,可以自行改成websocket server的局域网ip地址。

<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>推流页面</title>
</head>

<body>
  <video id="remoteStream" style="width: 320px; height: 240px;" autoplay muted></video>
  <button id="deviceBtn">打开本地摄像头</button>
  <button id="startBtn">建立连接</button>
  <script>
    // 显示本地画面的VideoElement
    let remoteStream = document.getElementById("remoteStream");

    // 订阅流用的Peerconnection
    let pc_sub = new RTCPeerConnection();

    let ws = new WebSocket('ws://127.0.0.1:9000');
    ws.addEventListener('open', () => {
      // 通知server pub已经上线
      ws.send(JSON.stringify({
        type: "subscribe"
      }))
    })

    ws.addEventListener('message', (event) => {
      let msg = JSON.parse(event.data);
      switch (msg.type) {
        case "start":
          break;

        case "offer":
          pc_sub.setRemoteDescription(msg).then(() => {
            pc_sub.createAnswer().then((answer) => {
              pc_sub.setLocalDescription(answer).then(() => {
                ws.send(JSON.stringify(answer));
              }).catch((err) => {

              })
            }).catch((err) => {
              console.error('create answer error', err);
            })
          }).catch((err) => {
            console.error('setRemoteDescription error', err);
          })
          break;

        default:
          pc_sub.addIceCandidate(msg);
          break;
      }
    })

    pc_sub.addEventListener('icecandidate', (event) => {
      if (event.candidate) {
        ws.send(JSON.stringify(event.candidate));
      }
    })

    pc_sub.addEventListener('track', (event) => {
      remoteStream.srcObject = event.streams[0];
    })

  </script>
</body>

</html>

其他

如果你也是专注前端多媒体或者对前端多媒体感兴趣,可以搜索微信公众号"前端多媒体"

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐