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

李超老师的项目

先肯定分为两个两个端,一个是服务器端一个是客户端。客户端用于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.mfbz.cn/a/693307.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

构建第一个ArkTS应用之@卡片使用动效能力

ArkTS卡片开放了使用动画效果的能力&#xff0c;支持显式动画、属性动画、组件内转场能力。需要注意的是&#xff0c;ArkTS卡片使用动画效果时具有以下限制&#xff1a; 表1 动效参数限制 名称 参数说明 限制描述 duration 动画播放时长 限制最长的动效播放时长为1秒&…

把文件从一台linux机器上传到另一台linux机器上

文章目录 1&#xff0c;第一种情况1.1 先测试2台机器是否可以互相通信1.2 对整个文件夹里面的所有内容进行传输的命令1.3 检查结果 2&#xff0c;第二种情况2.1&#xff0c;单个文件传输的命令 1&#xff0c;第一种情况 我这里有2台linux机器&#xff0c; 机器A&#xff1a;19…

NDIS Filter开发-PNP响应和安装

NDIS filter驱动可能是最容易生成的驱动之一&#xff0c;如果你安装了VS 2015 WDK之后&#xff0c;你可以直接生成一个能运行的Filter驱动&#xff0c;它一般是ndislwf。 和大部分硬件不同&#xff0c;NDIS Filter驱动介于软件和硬件抽象层之上&#xff0c;它和硬件相关&…

AI2THOR 2.1.0使用教程

一、安装和入门 1.1 AI2-THOR使用要求 操作系统&#xff1a; Mac OS X 10.9&#xff0c; Ubuntu 14.04显卡&#xff1a;DX9&#xff08;着色器型号 3.0&#xff09;或 DX11&#xff0c;功能级别为 9.3。CPU&#xff1a;支持 SSE2 指令集。Python 2.7 或 Python 3.5Linux 用户…

FuTalk设计周刊-Vol.040

&#x1f525;AI漫谈 热点捕手 1、零代码定制游戏NPC&#xff0c;百川智能发布角色大模型 百川智能此次推出了“角色创建平台搜索增强知识库”的定制化解决方案。通过这一方案&#xff0c;游戏厂商无需编写任何代码&#xff0c;只需通过简单的文字描述&#xff0c;便可以快速…

28-unittest批量执行(discover)

unittest框架提供了创建测试用例、测试套件以及批量执行的解决方案。 利用单元测试框架创建测试类&#xff0c;可以把每个测试方法看成是一个最小的单元&#xff0c; 由测试容器组装打包起来&#xff0c;然后可以统一执行&#xff0c;最后输出测试报告。 一、UnitTest核心要素…

OpenCV如何判断一张图片是否有过高的明暗变化

操作系统&#xff1a;ubuntu22.04OpenCV版本&#xff1a;OpenCV4.9IDE:Visual Studio Code编程语言&#xff1a;C11 前言 判断一张图片是否有过高的明暗变化&#xff0c;可以通过分析图像的亮度分布一致性来实现。一种常见的做法是计算图像的亮度标准差&#xff08;Standard …

JAVA技术设计模式

设计模式结构图 设计原则 职责单一原则接口隔离原则 一个类对另一个类的依赖应该建立在最小的接口上 依赖倒置面向接口编程,参数或变量,依赖注入,使用父类 开闭原则 对扩展开放(对提供方),对修改关闭(对使用方) 用抽象构建框架,用实现扩展细节 里氏替换原则…

计算机网络--物理层

计算机网络--计算机网络概念 计算机网络--物理层 计算机网络--数据链路层 计算机网络--网络层 计算机网络--传输层 计算机网络--应用层 1. 基本概念 物理层的概念&#xff1a;物理层解决如何在在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输…

STM32编程:实现LED灯闪烁(基于手写SDK的方式)

项目结构 stm32f10x.h 文件 //寄存器的值常常是芯片外设自动更改的&#xff0c;即使CPU没有执行程序&#xff0c;也有可能发生变化 //编译器有可能会对没有执行程序的变量进行优化//volatile表示易变的变量&#xff0c;防止编译器优化&#xff0c; #define __IO volati…

2024-6-9 石群电路-27

2024-6-9&#xff0c;星期日&#xff0c;12:49&#xff0c;天气&#xff1a;晴&#xff0c;心情&#xff1a;晴。Hello&#xff0c;大家&#xff0c;我回来啦&#xff0c;昨天断更了一天&#xff0c;是为什么捏&#xff0c;是因为&#xff0c;我通过毕业答辩啦&#xff01;&…

数据挖掘分析的一点进步分享

import pandas as pd import matplotlib.pyplot as plt import numpy as npdata pd.read_csv(heros.csv,encoding"gbk") data.head() 导入数据集 进行分析 df_datadata.copy() df_data.describe()df_data.info() df_data.drop(英雄,axis1,inplaceTrue) df_data[最…

[C++初阶]string类的详解

一、string类的模拟实现 上面已经对string类进行了简单的介绍&#xff0c;大家只要能够正常使用即可。在面试中&#xff0c;面试官总喜欢让我们来模拟实现string类&#xff0c;最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下string类的实现是…

Flink 问题之 No Watermark (Watermarks are only available if EventTime is used)

问题背景 Flink-1.17.0在集群下&#xff0c;获取Kafka集群数据&#xff0c;进行流模式实时计算&#xff0c;Watermarks提示&#xff1a;No Watermark (Watermarks are only available if EventTime is used)&#xff1b; source数据源是从kafka中读取topic数据&#xff0c;经…

【最新鸿蒙应用开发】——ArkUI两种开发范式

在进行鸿蒙应用开发&#xff0c;openHarmony提供了一种页面开发框架叫做ArkUI方舟框架来进行页面布局的开发。 ArkUI方舟UI框架 针对不同的应用场景及技术背景&#xff0c;方舟UI框架提供了两种开发范式&#xff0c;分别是基于ArkTS的声明式开发范式&#xff08;简称“声明式…

分享美好,高清无阻 - 直播极简联网解决方案

1、需求背景 随着移动互联网、UGC模式和直播平台的发展&#xff0c;网络直播的门槛日益降低&#xff0c;越来越多的人希望成为直播的主角。基于物联网的户外直播无线联网解决方案应运而生&#xff0c;满足直播者的需求。 户外直播无线联网解决方案提供了无处不在的直播体验&a…

UniAnimate:华科提出人类跳舞视频生成新框架,支持合成一分钟高清视频

节前&#xff0c;我们星球组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学。 针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 合集&#x…

Java 环境配置 -- Java 语言的安装、配置、编译与运行

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 002 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…

未来AI大模型的发展趋势

大家好&#xff0c;我是小悟 未来AI大模型的发展趋势无疑将是多元化、高效化、普及化以及人性化。随着技术的飞速进步&#xff0c;AI大模型将在各个领域中展现出更加广泛和深入的应用&#xff0c;成为推动社会进步的重要力量。 多元化是AI大模型发展的重要方向。随着数据量的…

vue26:vue的环境搭建

vue环境安装配置 在点击上方链接前&#xff0c;注意&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 下方的红字&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&am…