1V1音视频实时互动直播系统

news2024/11/18 16:49:04

李超老师的项目

先肯定分为两个两个端,一个是服务器端一个是客户端。客户端用于UI界面的显示,服务器端用于处理客户端发来的消息。

我们先搭建stun和turn服务器

首先介绍一下什么是stun协议,

它是用来干什么的?

stun协议存在的目的就是进行NAT穿越。stun是典型的客户端、服务器模式。客户端发送请求,服务器进行响应。

那么是什么NAT穿越呢?

首先我们先了解一下为什么要进行NAT穿越。下面举个例子,在两个浏览器之间进行实时的音视频互动,对于底层来说,这就是两个端点之间进行高效的网络传输。

为了解决音视频网络传输的问题,webrtc引入了一些网络传输协议。

1.NAT:那么此时我们介绍NAT,简单理解为将内网的地址转换为公网的地址,内网地址无法通讯,通过NAT转换为公网之后,才有通信的可能。

2.说到这里那么顺便介绍一下stun,这个stun充当的是中介的作用,在NAT的基础上,交换两个公网的信息,使得;两个公网之间可以建立连接。

3.turn,stun是有一定几率是不成功的,因此turn会在云端架设一个服务器,在p2p连接不成功的情况下,保证音视频的互通,它就相当于一个中转站。

4.ICE, 罗列所有通信可能性,选择最优解。

NAT又分为四种类型:

完全锥型

地址限制锥型

端口限制锥型

对称型

根据图中所示很好理解,caller与信令服务器连接,同时callee也跟信令服务器连接,第二个callee也跟信令服务器连接;caller向信令服务器发出join请求,信令服务器响应返回joined信息,第一个callee同理,但是第二个callee发送完join之后服务器发现成员已满,因此返回一个full信息,该callee不能join。然后caller和callee进行媒体协商,协商成功之后进行媒体流的数据传输。之后callee主动发出leave请求,服务器响应跟caller发出bye信息并返回callee leaved信息。

我们来看一下服务器端的代码。主要就是处理客户端发来的信令消息。也就是以上的流程转换为代码。

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

	socket.on('message', (room, data)=>{
		socket.to(room).emit('message',room, data);
	});

	socket.on('join', (room)=>{
		socket.join(room);
		var myRoom = io.sockets.adapter.rooms[room]; 
		var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
		logger.debug('the user number of room is: ' + users);

		if(users < USERCOUNT){
			socket.emit('joined', room, socket.id); //发给自己
			if(users > 1){
				socket.to(room).emit('otherjoin', room, socket.id);//发给除自己之外的房间内的所有人
			}
		
		}else{
			socket.leave(room);	
			socket.emit('full', room, socket.id);
		}
		//socket.emit('joined', room, socket.id); //发给自己
		//socket.broadcast.emit('joined', room, socket.id); //发给除自己之外的这个节点上的所有人
		//io.in(room).emit('joined', room, socket.id); //发给房间内的所有人
	});

	socket.on('leave', (room)=>{
		var myRoom = io.sockets.adapter.rooms[room]; 
		var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
		logger.debug('the user number of room is: ' + (users-1));
		//socket.emit('leaved', room, socket.id);
		//socket.broadcast.emit('leaved', room, socket.id);
		socket.to(room).emit('bye', room, socket.id);
		socket.emit('leaved', room, socket.id);
		//io.in(room).emit('leaved', room, socket.id);
	});

});

iceRestart是一个很好的方案,能够帮助我们重新选择数据传输的线路

下面了解一下状态机是什么这点蛮重要

下面介绍一下客户端代码编写的流程图

首先写函数start

start函数用于采集音视频数据

采集成功则与信令服务器连接,并注册信令函数

注册以上这些消息的处理函数

如果是joined则设置状态为joined,创建PC并绑定媒体流。

如果是otherjoin,则判断自身的状态是否为joined_unbind,如果是则需要重新创建PC并绑定媒体流并将状态设置为joined_conn,如果一开始状态为joined则直接将状态转换为joined_conn,接着开始媒体协商。

如果是full则状态设置为full并关闭PC,并关闭本地媒体流

客户端的实现需要注意的几点是

1.网络连接要在音视频数据获取到之后,否则可能导致绑定音视频流失败

2.当一端退出房间之后,另一端的PeerConnection要关闭重建,否则与新用户互通时媒体协商会失败。

3.所有的处理流程为异步处理

这里要了解一下什么是异步处理:

异步事件处理:要等待收到一个消息或事件后,才能做下一步的操作

同步处理:做完一步,直接做下一步

接下来我们介绍一下客户端的代码

首先先了解一个api

其中较为关键的是iceServers

第二个api

根据流程写以下代码:

首先start函数获取音视频数据

function start(){

	if(!navigator.mediaDevices ||
		!navigator.mediaDevices.getUserMedia){
		console.error('the getUserMedia is not supported!');
		return;
	}else {

		var constraints;

		if( shareDeskBox.checked && shareDesk()){

			constraints = {
				video: false,
				audio:  {
					echoCancellation: true,
					noiseSuppression: true,
					autoGainControl: true
				}
			}

		}else{
			constraints = {
				video: true,
				audio:  {
					echoCancellation: true,
					noiseSuppression: true,
					autoGainControl: true
				}
			}
		}

		navigator.mediaDevices.getUserMedia(constraints)
					.then(getMediaStream)
					.catch(handleError);
	}

}

与信令服务器连接,注册处理函数

function conn(){

	socket = io.connect();

	socket.on('joined', (roomid, id) => {
		console.log('receive joined message!', roomid, id);
		state = 'joined'

		//如果是多人的话,第一个人不该在这里创建peerConnection
		//都等到收到一个otherjoin时再创建
		//所以,在这个消息里应该带当前房间的用户数
		//
		//create conn and bind media track
		createPeerConnection();
		bindTracks();

		btnConn.disabled = true;
		btnLeave.disabled = false;
		console.log('receive joined message, state=', state);
	});

	socket.on('otherjoin', (roomid) => {
		console.log('receive joined message:', roomid, state);

		//如果是多人的话,每上来一个人都要创建一个新的 peerConnection
		//
		if(state === 'joined_unbind'){
			createPeerConnection();
			bindTracks();
		}

		state = 'joined_conn';
		call();

		console.log('receive other_join message, state=', state);
	});

	socket.on('full', (roomid, id) => {
		console.log('receive full message', roomid, id);
		hangup();
		closeLocalMedia();
		state = 'leaved';
		console.log('receive full message, state=', state);
		alert('the room is full!');
	});

	socket.on('leaved', (roomid, id) => {
		console.log('receive leaved message', roomid, id);
		state='leaved'
		socket.disconnect();
		console.log('receive leaved message, state=', state);

		btnConn.disabled = false;
		btnLeave.disabled = true;
	});

	socket.on('bye', (room, id) => {
		console.log('receive bye message', roomid, id);
		//state = 'created';
		//当是多人通话时,应该带上当前房间的用户数
		//如果当前房间用户不小于 2, 则不用修改状态
		//并且,关闭的应该是对应用户的peerconnection
		//在客户端应该维护一张peerconnection表,它是
		//一个key:value的格式,key=userid, value=peerconnection
		state = 'joined_unbind';
		hangup();
		offer.value = '';
		answer.value = '';
		console.log('receive bye message, state=', state);
	});

	socket.on('disconnect', (socket) => {
		console.log('receive disconnect message!', roomid);
		if(!(state === 'leaved')){
			hangup();
			closeLocalMedia();

		}
		state = 'leaved';
	
	});

	socket.on('message', (roomid, data) => {
		console.log('receive message!', roomid, data);

		if(data === null || data === undefined){
			console.error('the message is invalid!');
			return;	
		}

		if(data.hasOwnProperty('type') && data.type === 'offer') {
			
			offer.value = data.sdp;

			pc.setRemoteDescription(new RTCSessionDescription(data));

			//create answer
			pc.createAnswer()
				.then(getAnswer)
				.catch(handleAnswerError);

		}else if(data.hasOwnProperty('type') && data.type == 'answer'){
			answer.value = data.sdp;
			pc.setRemoteDescription(new RTCSessionDescription(data));
		
		}else if (data.hasOwnProperty('type') && data.type === 'candidate'){
			var candidate = new RTCIceCandidate({
				sdpMLineIndex: data.label,
				candidate: data.candidate
			});
			pc.addIceCandidate(candidate);	
		
		}else{
			console.log('the message is invalid!', data);
		
		}
	
	});


	roomid = getQueryVariable('room');
	socket.emit('join', roomid);

	return true;
}

媒体协商call函数

function call(){
	
	if(state === 'joined_conn'){

		var offerOptions = {
			offerToRecieveAudio: 1,
			offerToRecieveVideo: 1
		}

		pc.createOffer(offerOptions)
			.then(getOffer)
			.catch(handleOfferError);
	}
}
function getOffer(desc){
	pc.setLocalDescription(desc);
	offer.value = desc.sdp;
	offerdesc = desc;

	//send offer sdp
	sendMessage(roomid, offerdesc);	

}
function getAnswer(desc){
	pc.setLocalDescription(desc);
	answer.value = desc.sdp;

	//send answer sdp
	sendMessage(roomid, desc);
}

每个端都维护一个自己的peerconnection

var pcConfig = {
  'iceServers': [{
    'urls': 'turn:stun.al.learningrtc.cn:3478',
    'credential': "mypasswd",
    'username': "garrylea"
  }]
};
function createPeerConnection(){

	//如果是多人的话,在这里要创建一个新的连接.
	//新创建好的要放到一个map表中。
	//key=userid, value=peerconnection
	console.log('create RTCPeerConnection!');
	if(!pc){
		pc = new RTCPeerConnection(pcConfig);

		pc.onicecandidate = (e)=>{

			if(e.candidate) {
				sendMessage(roomid, {
					type: 'candidate',
					label:event.candidate.sdpMLineIndex, 
					id:event.candidate.sdpMid, 
					candidate: event.candidate.candidate
				});
			}else{
				console.log('this is the end candidate');
			}
		}

		pc.ontrack = getRemoteStream;
	}else {
		console.warning('the pc have be created!');
	}

	return;	
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

有没有全面的人、货、场零售数据分析方案?

有。奥威BI零售数据分析方案正是这样一套全面的人、货、场零售数据分析解决方案&#xff0c;旨在帮助零售企业实现数据驱动的决策&#xff0c;优化业务流程&#xff0c;提升市场竞争力。 一、方案概述 该零售数据分析方案基于先进的BI技术&#xff0c;预设了以人、货、场、供…

Ubuntu安装cuda

文章目录 前言一、安装NVIDIA驱动1.1 过程中的问题1.2 解决方法1.3 重启后出现 perform MOK management 二、安装Cuda2.1 检查是否安装显卡驱动2.2 安装Cuda2.3 验证CUDA是否安装成功 三、配置环境变量---未完2.4 图片居中加调整大学 总结 #pic_center 前言 只是为方便学习&…

【设计模式深度剖析】【2】【行为型】【命令模式】| 以打开文件按钮、宏命令、图形移动与撤销为例加深理解

&#x1f448;️上一篇:模板方法模式 设计模式-专栏&#x1f448;️ 文章目录 命令模式定义英文原话直译如何理解呢&#xff1f; 四个角色1. Command&#xff08;命令接口&#xff09;2. ConcreteCommand&#xff08;具体命令类&#xff09;3. Client&#xff08;客户端&…

Linux驱动开发笔记(六)中断子系统及实验

文章目录 前言一、中断子系统框架1. 中断硬件简单描述2. 中断的软件描述2.1 中断处理的两部分模型2.2 系统框架 二、GIC v3中断控制器1. GIC v3基本结构1.1 Distributor1.2 Redistributor1.3 ITS1.4 CPU interface 2. 中断类型与特点3. 中断号 三、函数编写3.1 相关API函数3.2 …

【自动驾驶】针对低速无人车的线控底盘技术

目录 术语定义 一般要求 操纵装置 防护等级 识别代号 技术要求 通过性要求 直线行驶稳定性 环境适应性要求 功能安全要求 信息安全要求 故障处理要求 通信接口 在线升级(OTA) 线控驱动 动力性能 驱动控制响应能力 线控制动 行车制动 制动响应能力 线控转向 总体要求 线控…

计算机网络 期末复习(谢希仁版本)第5章

**屏蔽作用&#xff1a;**运输层向高层用户屏蔽了下面网络核心的细节&#xff08;如网络拓扑、所采用的路由选择协议等&#xff09;&#xff0c;使应用进程看见的就是好像在两个运输层实体之间有一条端到端的逻辑通信信道。 10. 端口用一个 16 位端口号进行标志&#xff0c;允许…

力扣 496. 下一个更大元素 I

题目来源&#xff1a;https://leetcode.cn/problems/next-greater-element-i/description/ C题解1&#xff1a;暴力解决 class Solution { public:vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {int len1 nums1.size(…

C# WPF入门学习主线篇(六)—— TextBox常见属性和事件

欢迎回到C# WPF入门学习系列的第六篇。在前面的文章中&#xff0c;我们探讨了按钮&#xff08;Button&#xff09;的事件处理。今天&#xff0c;我们将继续学习另一个常用的WPF控件——TextBox。本文将介绍 TextBox 的常见属性和事件&#xff0c;并通过示例代码展示如何在实际应…

【Python数据类型的奥秘】:构建程序基石,驾驭信息之海

文章目录 &#x1f680;Python数据类型&#x1f308;1. 基本概念⭐2. 转化&#x1f44a;3. 数值运算&#x1f4a5;4. 数值运算扩展(math库常用函数) &#x1f680;Python数据类型 &#x1f308;1. 基本概念 整数&#xff08;int&#xff09;&#xff1a;整数是没有小数部分的数…

GRS认证是什么?GRS认证的要求是什么?

GRS认证&#xff08;Global Recycled Standard&#xff09;是全球回收标准认证&#xff0c;它是一个自愿性的、针对产品回收成分和供应链可追溯性的国际标准。GRS认证的目标是为纺织业的公司提供一个具有透明度和信任度的工具&#xff0c;以证明其产品的回收材料含量&#xff0…

C语言数据结构快速排序的非递归、归并排序、归并排序的非递归等的介绍

文章目录 前言一、快速排序非递归二、归并排序五、归并排序非递归总结 前言 C语言数据结构快速排序的非递归、归并排序、归并排序的非递归等的介绍 一、快速排序非递归 快速排序非递归的定义 快速排序非递归&#xff0c;需要使用栈来实现。将左右下标分别push到栈中。在栈为…

Redis-SDS 的定义

1.简介 Redis中的SDS&#xff08;Simple Dynamic String&#xff0c;简单动态字符串&#xff09;是一种专为Redis设计的字符串表示方式&#xff0c;旨在改进C语言中原生字符串的局限性。 2.SDS的核心结构 SDS结构不仅存储字符串的实际内容&#xff0c;还包括了额外的信息来优…

达梦数据库的配置与安装

1. 下载压缩包&#xff0c;点击链接产品下载 | 达梦数据库 (dameng.com) 本人只是测试需要&#xff0c;所以选择的是开发版&#xff08;X86平台&#xff09; 2. 解压压缩包 3.点击iso的应用 4.选择挂载 这个是因为我有装挂载软件&#xff0c;如果没有装&#xff0c;麻烦搜索下…

基于Raspi的Opencv-Python开发笔记

本文所有未强调 “windows终端” 的 “终端”字眼&#xff0c;都是默认树莓派的终端 系统版本 系统版本有必要强调一下&#xff0c;因为不同版本很多操作需要修改 在终端输入uname -a Release就是版本号&#xff0c;Codename是版本名 以下操作仅在此版本验证可行 使能摄像…

解决找不到api-ms-win-crt-runtime-l1-1-0.dll问题的5种方法

电脑已经成为我们生活和工作中不可或缺的工具&#xff0c;然而&#xff0c;由于各种原因&#xff0c;我们可能会遇到一些常见的问题&#xff0c;其中之一就是电脑缺失api-ms-win-crt-runtime-l1-1-0.dll文件。这个问题可能会导致电脑出现错误提示、程序无法正常运行等困扰。为了…

Zookeeper复习

一、入门 1、概念 zookeeper文件系统通知机制 2.特点 1&#xff09;、一个领导者&#xff0c;多个跟随者组成的集群。 2&#xff09;、集群中只要有半数以上存活机制&#xff0c;zookeeper集群能正产服务。zk适合安装奇数台。 3&#xff09;、全局数据一致&#xff1a;每…

一次 K8s 故障诊断:从 CPU 高负载到存储挂载泄露根源揭示

一、背景 现代软件部署中&#xff0c;容器技术已成为不可或缺的一环&#xff0c;在云计算和微服务架构中发挥着核心作用。随着容器化应用的普及&#xff0c;确保容器环境的可靠性成为了一个至关重要的任务。这就是容器SRE&#xff08;Site Reliability Engineering&#xff0c…

【强烈推荐】四元数与三维旋转

目录 1 强烈推荐讲解四元数与三维旋转的这篇文章&#xff0c;深入浅出2 笔记2.1 复数2.1.1 复数的定义2.1.2 复数的乘法与二维旋转 2.2 三维空间中的旋转2.2.1 角轴2.2.2 旋转的分解 2.3 四元数2.3.1 四元数的定义2.3.2 四元数的乘法2.3.3 四元数与三维旋转2.3.4 三维旋转的矩阵…

【Excel】Excel中将日期格式转换为文本格式,并按日期显示。

【问题需求】 在使用excel进行数据导入的过程中&#xff0c; 有的软件要求日期列必须是文本格式。 但是直接将日期列的格式改为文本后&#xff0c;显示一串数字&#xff0c;而不按日期显示。 进而无法导入使用。 【解决方法】 使用【TXET】函数公式进行处理&#xff0c; 在单…

转转回收业务策略中心的实践

1 背景 回收业务发展日益壮大&#xff0c;我们在邮寄、上门、门店三大履约模式下的业务逻辑日益复杂。同样都是在做回收这一个业务&#xff0c;即便履约方式不同&#xff0c;也有很多业务概念是一致的。为了避免各个业务闷头造轮子&#xff0c;同时又能拉齐三端的业务标准&…