webrtc之peerjs实现音视频通话

1,094 阅读6分钟

上一小节用Mesh方案实现了多人音视频,代码非常简单。然后再来看看如何使用 MCU方案来实现, 没钱!那就再来看看 SFU方案 如何实现, 还没那技术!这节就来看看如何使用 peerjs 库来实现音视频通话。这个库用起来相当方便,简直是有手就行!!但很奇怪,关于这个库的文章比较少,看了几篇都是几年前的???还是说我没找到???OK,到此为止。有我这一篇就够了,看了===会了。

这里先给出官方地址,它的github地址是这里,文档地址是这里,可自行前往。

介绍

peerjs库的示例代码很简单,使用起来主要分为两个部分:Data connectionsMedia calls,这俩也就对应peerConnectionDataChannelpeerConnectionMedia,也就是媒体流和数据通道。使用时只需要传入一个id,然后需要传输数据则调用peer.connect(),或者音视频则调用peer.call()方法。之后,peerjs就会自动帮你进行连接,省略了SDP交换和ICE候选的过程,直接一步到位给你连接上。你只需要监听对应的事件就行,传输数据监听data类型,音视频监听stream类型,相当简单,在事件中就能拿到数据或媒体流。

peerjs库还配有后端PeerServer库,当然你也可以自行实现。后端的话,也是非常简单,示例代码已经写的很详细了,这里就不说了。这里只提一个点,就是当你使用peerjs库和PeerServer库时,要注意单独给PeerServer起个服务。就是说你在后端要起两个服务,一个专门给peer使用,另一个才是socket服务。这里一定要注意,不然的话,你会发现连着一段时间后自动断开重连了,或者报错了。关于这个问题,废了好些劲,在论坛里找到2015年的帖子,仔细看了才解决的!!!帖子在这里,里面只是列出了大概的,但提到了解决方法。

剩下的,关于peer的事件和属性啥的,都在文档里写着,稍微看看就知道咋用了。还是看不懂的话,可以看看我的代码,下面会列出来。

实操

首先,这里先把后端给列出来,此代码是在webrtc之实现简单信令服务器的基础上新增的。加下面这段代码就行了

// 必须专门起个服务给 peerjs 不然会隔段时间 自动断开
const { ExpressPeerServer } = require("peer");
const peerApp = express();
const peerServer = http.createServer(peerApp);


peerApp.use("/",ExpressPeerServer(peerServer,{
  debug: true,
  path: "/myapp",
}));


peerServer.listen(8089,() => {
  console.log('服务启动成功 *:8089');
})
server.listen(9000,() => {
  console.log('服务启动成功 *:9000');
})

io.on('connection',(socket) => {
  onListener(socket)
})

OK,后端起个服务就行了。

再看看前端如何使用的,这里也是分为了音视频通话和消息发送两种。还记得之前的音视频通话过程吗?在进行通话和发消息之前先询问对方是否愿意,如果愿意再进行连接,不愿意就换一个。这里也是一样的逻辑,代码就不列出来了,忘了就往回看看。

当接收方收到发起方的询问时,如果选择了同意,那么会回复发起方,同时本地进行初始化peer。在open事件中可以拿到自动生成的id,有了ID再触发peerId事件,将ID发送给发起方。然后发起方监听peerId事件,触发onReceivePeerId事件进行初始化peer,与接收到的ID进行连接。

突然想到一个问题,既然peer的ID是可以自定义的,那就可以在回复发起方的询问时创建一个ID,然后在回复时一起发送过去,这样发起方就不用再额外监听peerId事件了,在收到接收方的回复时就可以进行peer的初始化了。唯一的点在于当发起方初始化后,接收方是否已经初始化完成了,他没初始化完成肯定是连接不上。那就还得一个事件来监听是否初始化完成,还是算了吧0.0

现在分析一下过程:

  1. 接收方在同意音视频/消息通讯后,进行初始化peer,然后监听一系列事件。并在peer初始化完成后将id发送给发起方。
  2. 发起方接收到回复后,同意的话则设置连接状态为连接中,然后等待接收方的peerId
  3. 发起方接收到peerId后,进行初始化peer,并连接对方。双方完成连接开始通讯。

看一看代码实现:

// 初始化 peer
const initPeer = (isReceiver = false, noticeType) => {
  const toUserId = remoteUserInfo.id; // 远程ID

  const randomId = getRandom();
  localPeer.value = new Peer(randomId, {
    host: "localhost",
    port: 8089,
    path: "/myapp",
    debug: 3,
  });
  localPeer.value.on("open", function (id) {
    if (isReceiver) {
      // 如果是接收方 将自己的id 发送给对方
      linkSocket.value.emit("peerId", {
        ...userInfo,
        peerId: id,
        toUserId,
        noticeType,
      });

      // data channel
      localPeer.value.on("connection", (conn) => {
        localPeerConn.value = conn;
        onConnEvent(conn);
      });

      // call channel
      localPeer.value.on("call", (call) => {
        localPeerCall.value = call;
        call.answer(localStream.value);
        onCallEvent(call, noticeType);
      });
    } else {
      if (noticeType === "message") {
        const conn = localPeer.value.connect(remoteUserInfo.peerId);
        localPeerConn.value = conn;
        onConnEvent(conn);
      } else {
        const call = localPeer.value.call(
          remoteUserInfo.peerId,
          localStream.value
        );
        localPeerCall.value = call;
        onCallEvent(call, noticeType);
      }
    }
  });

  // 监听错误事件
  localPeer.value.on("error", (error) => {
    console.log(error, "error");

    if (error.type === "network") {
      // 尝试重新连接
      // setTimeout(() => {
      //   localPeer.value.reconnect();
      // }, 5000);
    } else {
      handleOnError();
    }
  });

  // 监听关闭事件
  localPeer.value.on("close", () => {
    console.log("断开连接");
    handleOnClose();
  });
};

// 消息通信事件
const onConnEvent = (conn) => {
  // 连接打开时
  conn.on("open", () => {
    console.log("连接成功");

    remoteUserInfo.status = "1"; // 连接成功
    // 连接成功
    linkSocket.value.emit("connectionStatus", {
      ...userInfo,
      toUserId: remoteUserInfo.id,
      status: 1,
    });

    // 接收对方发送的消息
    conn.on("data", (data) => {
      console.log("receive", data);
      messageList.value.push({
        ...remoteUserInfo,
        toUserId: userInfo.id,
        msg: data,
      });
    });
  });

  // 连接关闭 一方关闭 另一方也可以接收到消息
  // conn.on("close", () => {
  //   console.log("断开连接");
  //   handleOnClose();
  // });

  // 连接错误
  // conn.on("error", (error) => {
  //   console.log(error, "error");
  //   handleOnError();
  // });
};

// 音视频通信事件
const onCallEvent = (call, noticeType) => {
  call.on("stream", (stream) => {
    console.log("收到对方的流", stream);
    setDomStream(noticeType, stream, true);
  });

  call.on("error", (error) => {
    console.log(error, "error");
    handleOnError();
  });

  call.on("close", () => {
    console.log("call close");
    handleOnClose();
  });
};

// 接收到对方的peerId
const onReceivePeerId = async (data) => {
  const { peerId, noticeType } = data;
  remoteUserInfo.peerId = peerId;
  remoteUserInfo.noticeType = noticeType;
  console.log("接收到对方peerId", peerId);

  try {
    if (noticeType !== "message") {
      await initDevices();
      await initMedia(noticeType);
    }

    // 初始化 peer
    initPeer(false, noticeType);
    userInfo.deviceStatus = 0;
  } catch (error) {
    console.log(error, "error");
    userInfo.deviceStatus = 5;
  }
};

到这里,双方的音视频通话就实现了,要加一些基本功能的话,可以参考之前写的webrtc之音视频实用功能

如果是双方发消息的话,可以参考下面这个代码:

const handleSendMsg = () => {
  if (!localPeerConn.value || !message.value) return;
  switch (remoteUserInfo.status) {
    case "3":
      ElMessage({
        message: "对方已离线!",
        type: "error",
      });
      break;
    case "4":
      ElMessage({
        message: "连接失败,请稍后再试!",
        type: "error",
      });
      break;
    case "5":
      ElMessage({
        message: "对方已离开!",
        type: "error",
      });
      break;
    default:
      localPeerConn.value.send(message.value);

      messageList.value.push({
        ...userInfo,
        toUserId: remoteUserInfo.id,
        msg: message.value,
      });

      message.value = "";
      break;
  }
};

小节

本小节实现了使用peerjs库来实现音视频通话和消息通讯。简单介绍了peerjs的使用,以及分析了其使用的过程,其实和原生使用webrtc没什么太大区别,只是屏蔽了SDP交换和ICE收集的过程。另外,你需要配置sslSTUN协议、TURN协议和一些服务端的配置也是可以加的。

peerjs简单,但是你并不知道其中的实现过程,出了问题比较难排查。而自己使用webrtc来实现,复杂,但你能清楚的知道每个步骤。

下一小节暂定