WebRTC入门

效果展示

WebRTC入门

基础概念

  • WebRTC指的是基于web的实时视频通话,其实就相当于A->B发直播画面,同时B->A发送直播画面,这样就是视频聊天了
  • WebRTC的视频通话是A和B两两之间进行的
  • WebRTC通话双方通过一个公共的中心服务器找到对方,就像聊天室一样
  • WebRTC的连接过程一般是
    1. A通过websocket连接下中心服务器,B通过websocket连接下中心服务器。每次有人加入或退出中心服务器,中心服务器就把为维护的连接广播给A和B
    2. A接到广播知道了B的存在,A发起提案,传递视频编码器等参数,让中心服务器转发给B。B收到中心服务器转发的A的提案,创建回答,传递视频编码器等参数,让中心服务器转发给A
    3. A收到回答,发起交互式连接,包括自己的地址,端口等,让中心服务器转发给B。B收到连接,回答交互式连接,包括自己的地址,端口等,让中心服务器转发给A。
    4. 至此A知道了B的地址,B知道了A的地址,连接建立,中心服务器退出整个过程
    5. A给B推视频流,同时B给A推视频流。双方同时用video元素把对方的视频流播放出来

API

  • WebSokcet 和中心服务器的连接,中心服务器也叫信令服务器,用来建立连接前中转消息,相当于相亲前的媒人

  • RTCPeerConnection 视频通话连接

  • rc.createOffer 发起方创建本地提案,获得SDP描述

  • rc.createAnswer 接收方创建本地回答,获得SDP描述

  • rc.setLocalDescription 设置本地创建的SDP描述

  • rc.setRemoteDescription 设置对方传递过来的SDP描述

  • rc.onicecandidate 在创建本地提案会本地回答时触发此事件,获得交互式连接对象,用于发送给对方

  • rc.addIceCandidate 设置中心服务器转发过来IceCandidate

  • rc.addStream 向连接中添加媒体流

  • rc.addTrack 向媒体流中添加轨道

  • rc.ontrack 在此事件中接受来自对方的媒体流

其实两个人通信只需要一个RTCPeerConnection,A和B各持一端,不需要两个RTCPeerConnection,这点容易被误导

媒体流

获取

这里我获取的是窗口视频流,而不是摄像头视频流

navigator.mediaDevices.getDisplayMedia()     .then(meStream => {         //在本地显示预览         document.getElementById("local").srcObject = meStream;     }) 

传输

         //给对方发送视频流         other.stream = meStream;         const videoTracks = meStream.getVideoTracks();         const audioTracks = meStream.getAudioTracks();         //log("推流")         other.peerConnection.addStream(meStream);         meStream.getVideoTracks().forEach(track => {             other.peerConnection.addTrack(track, meStream);         }); 

接收

other.peerConnection.addEventListener("track", event => {     //log("拉流")     document.getElementById("remote").srcObject = event.streams[0]; }) 

连接

WebSocet连接

这是最开始需要建立的和信令服务器的连接,用于点对点连接建立前转发消息,这算是最重要的逻辑了

ws = new WebSocket('/sdp'); ws.addEventListener("message", event => {     var msg = JSON.parse(event.data);     if (msg.type == "connect") {         //log("接到提案");         var other = remotes.find(r => r.name != myName);         onReciveOffer(msg.data.description, msg.data.candidate, other);     }     else if (msg.type == "connected") {         //log("接到回答");         var other = remotes.find(r => r.name != myName);         onReciveAnwer(msg.data.description, msg.data.candidate, other);     }     //获取自己在房间中的临时名字     else if (msg.type == "id") {         myName = msg.data;     }     //有人加入或退出房间时     else if (msg.type == "join") {         //成员列表         for (var i = 0; i < msg.data.length; i++) {             var other = remotes.find(r => r.name == msg.data[i]);             if (other == null) {                 remotes.push({                     stream: null,                     peerConnection: new RTCPeerConnection(null),                     description: null,                     candidate: null,                     video: null,                     name: msg.data[i]                 });             }         }         //过滤已经离开的人         remotes = remotes.filter(r => msg.data.find(x => x == r.name) != null);         //...     } }); 

RTCPeerConnection连接

在都已经加入聊天室后就可以开始建立点对点连接了

//对某人创建提案 other.peerConnection.createOffer({ offerToReceiveVideo: 1 })     .then(description => {         //设置成自己的本地描述         other.description = description;         other.peerConnection.setLocalDescription(description);     }); 

在创建提案后会触发此事件,然后把提案和交互式连接消息一起发送出去

//交互式连接候选项 other.peerConnection.addEventListener("icecandidate", event => {     other.candidate = event.candidate;     //log("发起提案");     //发送提案到中心服务器     ws.send(JSON.stringify({         type: "connect",         data: {             name: other.name,             description: other.description,             candidate: other.candidate         }     })); }) 

对方收到提案后按照同样的流程创建回答和响应

/**接收到提案 */ function onReciveOffer(description, iceCandidate,other) {     //交互式连接候选者     other.peerConnection.addEventListener("icecandidate", event => {         other.candidate = event.candidate;         //log("发起回答");         //回答信令到中心服务器         ws.send(JSON.stringify({             type: "connected",             data: {                 name: other.name,                 description: other.description,                 candidate: other.candidate             }         }));     })     //设置来自对方的远程描述     other.peerConnection.setRemoteDescription(description);     other.peerConnection.addIceCandidate(new RTCIceCandidate(iceCandidate));     other.peerConnection.createAnswer()         .then(answerDescription => {             other.description = answerDescription;             other.peerConnection.setLocalDescription(answerDescription);         }) } 

发起方收到回答后,点对点连接建立,双方都能看到画面了,至此已经不需要中心服务器了

/**接收到回答 */ function onReciveAnwer(description, iceCandidate,other) {     //收到回答后设置接收方的描述     other.peerConnection.setRemoteDescription(description);     other.peerConnection.addIceCandidate(new RTCIceCandidate(iceCandidate)); } 

完整代码

SDPController.cs
[ApiController] [Route("sdp")] public class SDPController : Controller {     public static List<(string name, WebSocket ws)> clients = new List<(string, WebSocket)>();     private List<string> names = new List<string>() { "张三", "李四", "王五","钟鸣" };      [HttpGet("")]     public async Task Index()     {         WebSocket client = await HttpContext.WebSockets.AcceptWebSocketAsync();         var ws = (name:names[clients.Count], client);         clients.Add(ws);         await client.SendAsync(UTF8Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new {type="id",data=ws.name})), WebSocketMessageType.Text, true, CancellationToken.None);         List<string> list = new List<string>();         foreach (var person in clients)         {             list.Add(person.name);         }         var join = new         {             type = "join",             data = list,         };         foreach (var item in clients)         {             await item.ws.SendAsync(UTF8Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(join)), WebSocketMessageType.Text, true, CancellationToken.None);         }          var defaultBuffer = new byte[40000];         try         {             while (!client.CloseStatus.HasValue)             {                 //接受信令                 var result = await client.ReceiveAsync(defaultBuffer, CancellationToken.None);                 JObject obj=JsonConvert.DeserializeObject<JObject>(UTF8Encoding.UTF8.GetString(defaultBuffer,0,result.Count));                 if (obj.Value<string>("type")=="connect" || obj.Value<string>("type") == "connected")                 {                     var another = clients.FirstOrDefault(r => r.name == obj["data"].Value<string>("name"));                     await another.ws.SendAsync(new ArraySegment<byte>(defaultBuffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);                 }             }         }         catch (Exception e)         {         }         Console.WriteLine("退出");         clients.Remove(ws);         list = new List<string>();         foreach (var person in clients)         {             list.Add(person.name);         }         join = new         {             type = "join",             data = list         };         foreach (var item in clients)         {             await item.ws.SendAsync(UTF8Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(join)), WebSocketMessageType.Text, true, CancellationToken.None);         }     } } 
home.html
<!DOCTYPE html> <html> <head>     <meta charset="utf-8" />     <title></title>     <style>         html,body{             height:100%;             margin:0;         }         .container{             display:grid;             grid-template:auto 1fr 1fr/1fr 200px;             height:100%;             grid-gap:8px;             justify-content:center;             align-items:center;         }         .video {             background-color: black;             height:calc(100% - 1px);             overflow:auto;         }         #local {             grid-area:2/1/3/2;         }         #remote {             grid-area: 3/1/4/2;         }         .list{             grid-area:1/2/4/3;             background-color:#eeeeee;             height:100%;             overflow:auto;         }         #persons{             text-align:center;         }         .person{             padding:5px;         }     </style> </head> <body>     <div class="container">         <div style="grid-area:1/1/2/2;padding:8px;">             <button id="start">录制本地窗口</button>             <button id="call">发起远程</button>             <button id="hangup">挂断远程</button>         </div>         <video autoplay id="local" class="video"></video>         <video autoplay id="remote" class="video"></video>         <div class="list">             <div style="text-align:center;background-color:white;padding:8px;">                 <button id="join">加入</button>                 <button id="exit">退出</button>             </div>             <div id="persons">              </div>             <div id="log">              </div>         </div>     </div>      <script>         /**在屏幕顶部显示一条消息,3秒后消失 */         function layerMsg(msg) {             // 创建一个新的div元素作为消息层             var msgDiv = document.createElement('div');             msgDiv.textContent = msg;              // 设置消息层的样式             msgDiv.style.position = 'fixed';             msgDiv.style.top = '0';             msgDiv.style.left = '50%';             msgDiv.style.transform = 'translateX(-50%)';             msgDiv.style.background = '#f2f2f2';             msgDiv.style.color = '#333';             msgDiv.style.padding = '10px';             msgDiv.style.borderBottom = '2px solid #ccc';             msgDiv.style.width = '100%';             msgDiv.style.textAlign = 'center';             msgDiv.style.zIndex = '9999'; // 确保消息层显示在最顶层              // 将消息层添加到文档的body中             document.body.appendChild(msgDiv);              // 使用setTimeout函数,在3秒后移除消息层             setTimeout(function () {                 document.body.removeChild(msgDiv);             }, 3000);         }         function log(msg) {             document.getElementById("log").innerHTML += `<div>${msg}</div>`;         }     </script>      <script>         var myName = null;         // 服务器配置         const servers = null;         var remotes = [];         var startButton = document.getElementById("start");         var callButton = document.getElementById("call");         var hangupButton = document.getElementById("hangup");         var joinButton = document.getElementById("join");         var exitButton = document.getElementById("exit");         startButton.disabled = false;         callButton.disabled = false;         hangupButton.disabled = true;         joinButton.disabled = false;         exitButton.disabled = true;          /**和中心服务器的连接,用于交换信令 */         var ws;         //加入房间         document.getElementById("join").onclick = function () {             ws = new WebSocket('/sdp');             ws.addEventListener("message", event => {                 var msg = JSON.parse(event.data);                 if (msg.type == "offer") {                     log("接收到offer");                     onReciveOffer(msg);                 }                 else if (msg.type == "answer") {                     log("接收到answer");                     onReciveAnwer(msg);                 }                 else if (msg.candidate != undefined) {                     layerMsg("接收到candidate");                     onReciveIceCandidate(msg);                 }                 else if (msg.type == "connect") {                     log("接到提案");                     var other = remotes.find(r => r.name != myName);                     onReciveOffer(msg.data.description, msg.data.candidate, other);                 }                 else if (msg.type == "connected") {                     log("接到回答");                     var other = remotes.find(r => r.name != myName);                     onReciveAnwer(msg.data.description, msg.data.candidate, other);                 }                 else if (msg.type == "id") {                     myName = msg.data;                 }                 else if (msg.type == "join") {                     //新增                     for (var i = 0; i < msg.data.length; i++) {                         var other = remotes.find(r => r.name == msg.data[i]);                         if (other == null) {                             remotes.push({                                 stream: null,                                 peerConnection: new RTCPeerConnection(servers),                                 description: null,                                 candidate: null,                                 video: null,                                 name: msg.data[i]                             });                         }                     }                     //过滤已经离开的人                     remotes = remotes.filter(r => msg.data.find(x => x == r.name) != null);                     document.getElementById("persons").innerHTML = "";                     for (var i = 0; i < remotes.length; i++) {                         var div = document.createElement("div");                         div.classList.add("person")                         var btn = document.createElement("button");                         btn.innerText = remotes[i].name;                         if (remotes[i].name == myName) {                             btn.innerText += "(我)";                         }                         div.appendChild(btn);                         document.getElementById("persons").appendChild(div);                     }                 }             });             startButton.disabled = false;             joinButton.disabled = true;             exitButton.disabled = false;          }         //退出房间         document.getElementById("exit").onclick = function () {             if (ws != null) {                 ws.close();                 ws = null;                 startButton.disabled = true;                 callButton.disabled = true;                 hangupButton.disabled = true;                 joinButton.disabled = false;                 exitButton.disabled = true;                 document.getElementById("persons").innerHTML = "";                 remotes = [];                 local.peerConnection = null;                 local.candidate = null;                 local.description = null;                 local.stream = null;                 local.video = null;             }         }          //推流         startButton.onclick = function () {             var local = remotes.find(r => r.name == myName);             var other = remotes.find(r => r.name != myName);             if (other == null) {                 return;             }             navigator.mediaDevices.getDisplayMedia()                 .then(meStream => {                     //在本地显示预览                     document.getElementById("local").srcObject = meStream;                     //给对方发送视频流                     other.stream = meStream;                     const videoTracks = meStream.getVideoTracks();                     const audioTracks = meStream.getAudioTracks();                     log("推流")                     other.peerConnection.addStream(meStream);                     meStream.getVideoTracks().forEach(track => {                         other.peerConnection.addTrack(track, meStream);                     });                 })         }         callButton.onclick = function () {             callButton.disabled = true;             hangupButton.disabled = false;             var other = remotes.find(r => r.name != myName);             //交互式连接候选者             other.peerConnection.addEventListener("icecandidate", event => {                 if (event.candidate == null) {                     return;                 }                 other.candidate = event.candidate;                 log("发起提案");                 //发送提案到中心服务器                 ws.send(JSON.stringify({                     type: "connect",                     data: {                         name: other.name,                         description: other.description,                         candidate: other.candidate                     }                 }));             })             other.peerConnection.addEventListener("track", event => {                 log("拉流")                 document.getElementById("remote").srcObject = event.streams[0];             })             //对某人创建信令             other.peerConnection.createOffer({ offerToReceiveVideo: 1 })                 .then(description => {                     //设置成自己的本地描述                     other.description = description;                     other.peerConnection.setLocalDescription(description);                 })                 .catch(e => {                     debugger                 });         }         //挂断给对方的流         hangupButton.onclick = function () {             callButton.disabled = false;             hangupButton.disabled = true;             var local = remotes.find(r => r.name == myName);             var other = remotes.find(r => r.name != myName);             other.peerConnection = new RTCPeerConnection(servers);             other.description = null;             other.candidate = null;             other.stream = null;         }          /**接收到回答 */         function onReciveAnwer(description, iceCandidate,other) {             if (other == null) {                 return;             }             //收到回答后设置接收方的描述             other.peerConnection.setRemoteDescription(description)                 .catch(e => {                     debugger                 });             other.peerConnection.addIceCandidate(new RTCIceCandidate(iceCandidate));         }          /**接收到提案 */         function onReciveOffer(description, iceCandidate,other) {             //交互式连接候选者             other.peerConnection.addEventListener("icecandidate", event => {                 if (event.candidate == null) {                     return;                 }                 other.candidate = event.candidate;                 log("发起回答");                 //回答信令到中心服务器                 ws.send(JSON.stringify({                     type: "connected",                     data: {                         name: other.name,                         description: other.description,                         candidate: other.candidate                     }                 }));             })             other.peerConnection.addEventListener("track", event => {                 log("拉流")                 document.getElementById("remote").srcObject = event.streams[0];             })             //设置来自对方的远程描述             other.peerConnection.setRemoteDescription(description)                 .catch(e => {                     debugger                 });             other.peerConnection.addIceCandidate(new RTCIceCandidate(iceCandidate));             other.peerConnection.createAnswer()                 .then(answerDescription => {                     other.description = answerDescription;                     other.peerConnection.setLocalDescription(answerDescription);                 })         }          function onReciveIceCandidate(iceCandidate) {             if (remotePeerConnection == null) {                 return;             }             remotePeerConnection.addIceCandidate(new RTCIceCandidate(iceCandidate));         }     </script> </body> </html> 
发表评论

评论已关闭。

相关文章