WebRTC入门

news/2024/10/6 3:44:50/文章来源:https://www.cnblogs.com/ggtc/p/18287925

效果展示

image

基础概念

  • 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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/739669.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

组装8 地图移动

8,地图移动, 建立一个SURFACE,大小是18* unitx 19* unity 地图坐标 X,Y 坐标在显示中间 读取这个坐标 18 * 19 范围的地图数据,贴图到SURFACE 上。 问题 1,OBJECT第三层的贴图是UNITX,HEIGHT的大小, 这个HEIGHT的高度需要读取超过19个UNITY 的OBJECT,应该+12就可…

KubeSphere 社区双周报|2024.06.21-07.04

KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书、新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列社区动态。 本次双周报涵盖时间为:2024.06.21-07.04。 贡献者名单新晋 KubeSphere co…

HSQL 数据库介绍(1)--简介

HSQLDB(HyperSQL Database)是一款用 Java 编写的关系数据库管理系统;它提供了许多功能,并严格遵循最新的 SQL 和 JDBC 4.2 标准;本文主要介绍其基本概念及安装。 1、简介 HyperSQL Database(HSQLDB)是一款现代的关系数据库系统。HSQLDB 遵循国际 ISO SQL:2016 标准,支持…

lazarus 设置中文界面及开启代码提示

1.选择, Tools-Options-Environment-General-Language 选择Chinese[zh-CN],点击ok,重启即可 2.开启标识符补全,代码提示,如下图设置即可 本人小站:www.shibanyan.com

《Programming from the Ground Up》阅读笔记:p19-p48

《Programming from the Ground Up》学习第2天,p19-p48总结,总计30页。 一、技术总结 1.object file p20, An object file is code that is in the machines language, but has not been completely put together。 之前在很多地方都看到object file这个概念,但都没有看到起…

Qt/C++音视频开发78-获取本地摄像头支持的分辨率/帧率/格式等信息/mjpeg/yuyv/h264

一、前言 上一篇文章讲到用ffmpeg命令方式执行打印到日志输出,可以拿到本地摄像头设备信息,顺藤摸瓜,发现可以通过执行 ffmpeg -f dshow -list_options true -i video="Webcam" 命令获取指定摄像头设备的分辨率帧率格式等信息,会有很多条。那为什么需要这个功能呢…

Lazarus的安装

推荐安装秋风绿色版lazarus,秋风的blog上有绿色版百度网盘的下载地址,对于没有VIP会员的可以去QQ群下载,群号:103341107,速度比网盘好些 下载完成后,推荐解压到非系统盘根目录,在根目录里找到“lazarus绿化工具-x86_64-win64.exe”并运行。上图的路径是你的程序所在目录…

关于电源的基础知识

基础知识很多时候,都没有直接的作用。但是不积跬步无以至千里,不积小流无以成江海。接下来就用一页笔记,简单说明一下不理想源的输出阻抗。在一个电路系统中,前级和后级的连接,大多需要计算输入输出阻抗的。

Denso Create Programming Contest 2024(AtCoder Beginner Contest 361)

Denso Create Programming Contest 2024(AtCoder Beginner Contest 361)\(A\) Insert \(AC\)循环结构。点击查看代码 int a[200]; int main() {int n,k,x,i;cin>>n>>k>>x;for(i=1;i<=n;i++){cin>>a[i];cout<<a[i]<<" ";i…

浅谈进程隐藏技术

在之前几篇文章已经学习了解了几种钩取的方法,这篇文章就利用钩取方式完成进程隐藏的效果。在实现进程隐藏时,首先需要明确遍历进程的方法。前言 在之前几篇文章已经学习了解了几种钩取的方法 ● 浅谈调试模式钩取 ● 浅谈热补丁 ● 浅谈内联钩取原理与实现 ● 导入地址表钩取…

什么是新质生产力

什么是新质生产力

好消息!数据库管理神器 Navicat 推出免费精简版:Navicat Premium Lite

前言 好消息,前不久Navicat推出了免费精简版的数据库管理工具Navicat Premium Lite,可用于商业和非商业目的,我们再也不需要付费、找破解版或者找其他免费平替工具了,有需要的同学可以马上下载使用起来。 工具官方介绍 Navicat Premium Lite 是 Navicat 的精简版,它包含了…