uniapp开发WebRTC语音直播间支持app(android+IOS)和H5,并记录了所有踩得坑

这篇具有很好参考价值的文章主要介绍了uniapp开发WebRTC语音直播间支持app(android+IOS)和H5,并记录了所有踩得坑。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、效果图

uni-app andriod webrtc,uniapp,vue,uni-app,webrtcuni-app andriod webrtc,uniapp,vue,uni-app,webrtc

uni-app andriod webrtc,uniapp,vue,uni-app,webrtcuni-app andriod webrtc,uniapp,vue,uni-app,webrtcuni-app andriod webrtc,uniapp,vue,uni-app,webrtc

二、主要功能

1. 创建自己的语音直播间

2. 查询所有直播间列表

3.加入房间

4.申请上位

5.麦克风控制

6.声音控制

7.赠送礼物(特效 + 批量移动动画)

8.退出房间

三、原理

1.uniapp 实现客户端H5、安卓、苹果

2.webRTC实现语音直播间(具体原理网上有很多文章我就不讲了,贴个图)

uni-app andriod webrtc,uniapp,vue,uni-app,webrtc

3.使用node.js搭建信令服务器(我用的是socket)

4.礼物及特效使用svga

四、踩坑及解决方案

1. 客户端(这里重点在于app端)一定要在视图层创建webRTC!!!不要在逻辑层创建!!!因为会要求使用安全连接,也就是说要用到SSL证书,这个很多人都没有,有的话当我没说。如何在视图层创建RTC呢?在uniapp中使用renderjs!

<script module="webRTC" lang="renderjs">
new RTCPeerConnection(iceServers)
</script>

2. (这里重点也在于app)客户端创建和信令服务器进行通信的socket时app端在页面跳转后socket状态消失无法响应信令服务器消息。解决方案是:一定不要在客户端视图层创建socket!!!也就是说socket不要创建在renderjs里,要在逻辑层用uniapp提供的api进行创建,然后使用uniapp文档中说明的逻辑层和视图层的通信方式进行通信,这样虽然在开发中有些繁琐,但是能解决问题。

onShow(){
// socketTask是使用uniapp提供的uni.connectSocket创建出来的socket实例
// watchSocketMessage代理了socket实例的onMessage方法
socketTask.watchSocketMessage = (data) => {
				this.watchSocketMessage(data)
			}
    
}

methed:{
    watchSocketMessage(){
        // 这里是收到信令服务器socket后的逻辑
    }
}
// 这里是逻辑层和renderjs通信的方式,通过监听状态的改变从而触发renderjs的对应的方法
// 注意在页面刚加载完成后这些方法会被默认触发一边,所以要在这些放方法做好判断return出去

<view :rid="rid" :change:rid="webRTC.initRid" :userId="userId" :change:userId="webRTC.initUserId"
			:giftnum="giftnum" :change:giftnum="webRTC.initgiftnum" :micPosition="micPosition"
			:change:micPosition="webRTC.initMicPositions" :giftPosition="giftPosition"
			:change:giftPosition="webRTC.initGiftPosition" :RTCJoin="RTCJoin" :change:RTCJoin="webRTC.changeRTCjoin"
			:RTCOffier="RTCOffier" :change:RTCOffier="webRTC.changeRTCoffier" :RTCAnswer="RTCAnswer" :isAudio="isAudio"
			:change:isAudio="webRTC.changeIsAudio" :change:RTCAnswer="webRTC.changeRTCAnswer"
			:RTCCandidate="RTCCandidate" :change:RTCCandidate="webRTC.changeRTCCandidate" :isTrue="isTrue"
			:change:isTrue="webRTC.changeIsTrue" :newMess="newMess" :change:newMess="webRTC.changeNewMessage"
			:isMedia="isMedia" :name="name" :change:name="webRTC.changeName" :change:isMedia="webRTC.changeIsMedia"
			:animos="animos" :change:animos="changeAnimos" class="chat">
</view>

3.连接顺序的问题,一定是:新进入的用户通过信令服务器给房间已有用户发送Offer,用户接收到Offer回应Answer,记住这个逻辑!

4.因为webRTC是运行在视图层的(也就是浏览器),而苹果默认浏览器是Safari,Safari浏览器默认机制是在用户主动和页面进行交互后,自动播放声音才会生效(也就是才有声音),所以在IOS端所有用户进入直播房间后默认都是静音的,用户主动开启音频才会受到直播间的声音(这是目前我发现的最好的解决办法)

五、核心代码(只有关键步骤)

1. 客户端socket

const socketTask = {
	socket: null,
	connect: () => {

		getApp().globalData.socket = uni.connectSocket({
			url:'ws://180.76.158.110:9000/socket/websocketv',
			// url: 'ws://192.168.3.254:9000/socket/websocketv',
			complete: (e) => {
				console.log(e);
			},
		});

		getApp().globalData.socket.onOpen((data) => {
			console.log("111111111");
			getApp().globalData.socket.send({
				data: JSON.stringify({
					type: "newConnect",
					userId: uni.getStorageSync('user').id,
				})
			})
		})

		getApp().globalData.socket.onClose((res) => {
			console.log("连接关闭", res);
			getApp().globalData.socket = null;
			setTimeout(() => {
				socketTask.connect()
			}, 3000)
		})

		getApp().globalData.socket.onError((err) => {
			console.log("连接异常", err);
			getApp().globalData.socket = null;
			setTimeout(() => {
				socketTask.connect()
			}, 1)
		})

		getApp().globalData.socket.onMessage((data) => {
			socketTask.watchSocketMessage(data)
		})

	},
	start: function() {
		this.connect()
	},
	watchSocketMessage: function() {
		// 这里实现自己的业务逻辑
	}
}

export default socketTask

2.客户端房间列表页

async onShow() {
			if (!getApp().globalData.socket) {
				await socketTask.start();
			}
			socketTask.watchSocketMessage = (data) => {
				console.log("===========收到新消息==========",data);
				this.watchSocketMessages(data)
			}
		},
methed:{
// 监听socket消息
			watchSocketMessages(res) {
				try {
					const socket_msg = JSON.parse(res.data);
					console.log("收到新消息", socket_msg);
					switch (socket_msg.type) {
						case "homeList":
							if (socket_msg.data.length == 0) {
								this.homeList = [];
								uni.showToast({
									title: "暂无房间,快去创建一个吧",
									icon: "none"
								})
							} else {
								this.homeList = socket_msg.data;
							}
							break
						case "leave":
							getApp().globalData.socket.send({
								data: JSON.stringify({
									type: "homeList",
									userId: this.userInfo.userId,
								})
							})
							break
						case "createSuccess":
							uni.redirectTo({
								url: `broadRoom?rid=${socket_msg.data.groupId}&&userId=${this.userInfo.id}&&groupInfo=${JSON.stringify(socket_msg.data)}`
							})
							break
					}
				} catch (e) {

				}
			},
}

3.客户端直播间

逻辑层:

async onShow() {
			const that = this;
			if (!getApp().globalData.socket) {
				console.log("socket不存在,重新连接");
				await socketTask.start();
			}
			socketTask.watchSocketMessage = (data) => {
				this.watchSocketMessage(data)
			}
			// 编译平台信息
			uni.getSystemInfo({
				success(res) {
					console.log("当前平台是", res);
					if (res.osName == 'ios') {
						console.log("我是ios", res)
						that.isMedia = 'ios';
					} else {
						console.log("我是安卓", res)
						that.isMedia = 'android';
					}

				}
			})

		}

methed:{
async watchSocketMessage(date) {
				const data = JSON.parse(date.data);
				switch (data.type) {
					case "join":
						console.log("join成功", data);
						this.newMessaGes(data);
						this.setUserList(data.admin);
						this.updataNewMic(data)
						// 找出自己以外的其他用户
						const arr = this.userList.filter((item, index) => {
							return item.userId !== this.userId
						})
						console.log("找出自己以外的其他用户", arr)
						// 通知renderjs层创建RTC
						this.RTCJoin = arr;
						this.updataIsShow()
						break

					case "newjoin":
						this.newMessaGes(data);
						this.setUserList(data.admin);
						break

					case "offer":
						//通知renderjs层有新人进入创建answer
						console.log("收到offer", data)
						this.RTCOffier = data;
						break
					case "answer":
						// 找到对应peer,设置answer
						console.log("收到offer", data)
						this.RTCAnswer = data;
						break
					case "candidate":
						// 找到对应的peer,将candidate添加进去
						this.RTCCandidate = data;
						break
					case "leave":
						if (data.data == "房主已解散房间") {
							this.closesAdmin()
						} else {
							const datas = {
								data,
							}
							this.newMessaGes(datas)
							this.setUserList(data.admin);
							this.updataNewMic(data);
						}
						break
					case "apply-admin":
						this.updataIsApply(data.data)
						break
					case "newMic":
						this.updataNewMic(data)
						break
					case "uplMicro":
						this.updataNewMic(data)
						break
					case "newMessage":
						this.newMess = data;
						break
				}
			},
}

视图层:

<script module="webRTC" lang="renderjs">

// 以下方法都在methed:{}中



// 监听changeRTCCandidate
			async changeRTCCandidate(data) {
				if (!data) {
					return
				}
				console.log("this.otherPeerConnections", this.otherPeerConnections);
				let arrs = this.otherPeerConnections.concat(this.myPeerConnections);


				if (arrs.length == 0) {
					return
				}

				let peerr = arrs.filter(item => {
					return item.otherId == data.userId
				})
				
				if (peerr[0].peer == {}) {
					return
				} else {
					console.log("candidatecandidate", data.candidate)
					await peerr[0].peer.addIceCandidate(new RTCIceCandidate(data.candidate))
				}
			},
			// 监听answer,找到对应peer设置answer
			async changeRTCAnswer(data) {
				if (!data) {
					return
				}
				let peers = this.myPeerConnections.filter(item => {
					return item.otherId == data.userId
				})
				console.log("peers[0]", peers[0])
				await peers[0].peer.setRemoteDescription(new RTCSessionDescription(data.answer))
			},
			// 监听offier,RTCAnswer的创建
			async changeRTCoffier(data) {
				if (!data) {
					return
				}
				let pear = null;
				try {
					pear = new RTCPeerConnection(iceServers);
				} catch (e) {
					console.log("实例化RTC-pear失败", e);
				}

				// 将音频流加入到Peer中
				this.localStream.getAudioTracks()[0].enabled = this.isTrue;
				this.localStream.getTracks().forEach(
					(track) => pear.addTrack(track, this.localStream)
				);
				this.otherPeerConnections.push({
					peer: pear,
					otherId: data.userId
				})
				//当远程用户向对等连接添加流时,我们将显示它
				pear.ontrack = (event) => {
					// 为该用户创建audio
					const track = event.track || event.streams[0]?.getTracks()[0];
					if (track && track.kind === 'audio') {
						console.log("存在音轨", event.streams[0]);
						this.renderAudio(data.userId, event.streams[0]);
					} else {
						console.warn("No audio track found in the received stream.");
					}
				};

				// 通过监听onicecandidate事件获取candidate信息
				pear.onicecandidate = async (event) => {
					if (event.candidate) {
						// 通过信令服务器发送candidate信息给用户B
						await this.$ownerInstance.callMethod("sendCandidate", {
							type: "candidate",
							userId: this.userId,
							rid: this.rid,
							msg: event.candidate,
							formUserId: data.userId,
						})
					}
				}

				pear.setRemoteDescription(new RTCSessionDescription(data.offer))

				// 接收端创建answer并发送给发起端
				pear.createAnswer().then(answer => {
					pear.setLocalDescription(answer);
					// 通知serve层给房间用户发送answer
					this.$ownerInstance.callMethod("sendAnswer", {
						type: "answer",
						userId: this.userId,
						rid: this.rid,
						msg: answer,
						formUserId: data.userId,
					})
				})
			},
			// 发起连接申请,offier的创建
			changeRTCjoin(RTCjoin) {
				if (!RTCjoin) {
					return
				}
				RTCjoin.forEach((item, index) => {
					let peer = null;
					try {
						peer = new RTCPeerConnection(iceServers);
					} catch (e) {
						console.log("实例化RTC失败", e);
					}

					this.localStream.getAudioTracks()[0].enabled = this.isTrue;
					this.localStream.getTracks().forEach(
						(track) => peer.addTrack(track, this.localStream)
					);

					peer.ontrack = (event) => {
						console.log("发起连接申请,offier的创建:peer.ontrack");
						const track = event.track || event.streams[0]?.getTracks()[0];
						if (track && track.kind === 'audio') {
							console.log("存在音轨2", event.streams[0]);
							this.renderAudio(item.userId, event.streams[0]);
						} else {
							console.warn("No audio track found in the received stream.");
						}
					};

					// 通过监听onicecandidate事件获取candidate信息
					peer.onicecandidate = (event) => {
						if (event.candidate) {
							// 通过信令服务器发送candidate信息给用户B
							this.$ownerInstance.callMethod("sendCandidate", {
								type: "candidate",
								userId: this.userId,
								rid: this.rid,
								msg: event.candidate,
								formUserId: item.userId,
							})
						}
					}
					this.myPeerConnections.push({
						peer: peer,
						otherId: item.userId
					})

					peer.createOffer(this.offerOptions).then(offer => {
						peer.setLocalDescription(offer);
						// 通知serve层给房间用户发送offier
						this.$ownerInstance.callMethod("sendOffier", {
							type: "offer",
							userId: this.userId,
							rid: this.rid,
							msg: offer,
							formUserId: item.userId,
						})
					})
				})
			},

			renderAudio(uid, stream) {
				let audio2 = document.getElementById(`audio_${uid}`);
				console.log("audio_name", `audio_${uid}`);
				if (!audio2) {
					audio2 = document.createElement('audio');
					audio2.id = `audio_${uid}`;
					audio2.setAttribute("webkit-playsinline", "");
					audio2.setAttribute("autoplay", true);
					audio2.setAttribute("playsinline", "");
					audio2.onloadedmetadata = () => {
						if (this.isAudio == 1) {
							console.log("不自动播放");
							audio2.pause();
						} else {
							audio2.play();
						}
					};

					this.audioList.push(audio2)
				}
				if ("srcObject" in audio2) {
					console.log("使用了srcObject赋值");
					audio2.srcObject = stream;
				} else {
					console.log("找不到srcObject赋值");
					audio2.src = window.URL.createObjectURL(stream);
				}
			},
async initMedia() {
				const that = this;

				console.log("##########", this.isMedia);
				// #ifdef APP-PLUS
				if (this.isMedia == 'android') {
					console.log("androidandroidandroidandroid");
					await plus.android.requestPermissions(
						['android.permission.RECORD_AUDIO'],
						async (resultObj) => {
								var result = 0;
								for (var i = 0; i < resultObj.granted.length; i++) {
									var grantedPermission = resultObj.granted[i];
									result = 1
								}
								for (var i = 0; i < resultObj.deniedPresent.length; i++) {
									var deniedPresentPermission = resultObj.deniedPresent[i];
									result = 0
								}
								for (var i = 0; i < resultObj.deniedAlways.length; i++) {
									var deniedAlwaysPermission = resultObj.deniedAlways[i];
									result = -1
								}
								that.localStream = await that.getUserMedia();
								that.$ownerInstance.callMethod("sendJoin", {
									type: "join",
									userId: that.userId,
									rid: that.rid,
									name: that.name
								})
							},
							function(error) {
								console.log("导入android出现错误", error);
							}
					);
				} else {
					console.log("iosiosiosiosiosios");
					that.localStream = await that.getUserMedia().catch(err => {
						console.log("出错了", err);
					})
					that.$ownerInstance.callMethod("sendJoin", {
						type: "join",
						userId: that.userId,
						rid: that.rid,
						name: that.name
					})
				}

				// #endif
				// #ifdef H5
				that.localStream = await that.getUserMedia();
				// 通知serve层加入成功
				this.$ownerInstance.callMethod("sendJoin", {
					type: "join",
					userId: this.userId,
					rid: this.rid,
					name: this.name
				})
				// #endif
			},
			getUserMedia(then) {
				return new Promise((resolve, reject) => {
					navigator.mediaDevices.getUserMedia(this.mediaConstraints).then((stream) => {
						return resolve(stream);
					}).catch(err => {
						if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
							// 用户拒绝了授权
							reject(new Error('用户拒绝了访问摄像头和麦克风的请求'));
						} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
							// 没有找到摄像头或麦克风
							reject(new Error('没有找到摄像头或麦克风'));
						} else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
							// 摄像头或麦克风不可读
							reject(new Error('摄像头或麦克风不可读'));
						} else if (err.name === 'OverconstrainedError' || err.name ===
							'ConstraintNotSatisfiedError') {
							// 由于媒体流的约束条件无法满足,请求被拒绝
							reject(new Error('请求被拒绝,由于媒体流的约束条件无法满足'));
						} else if (err.name === 'TypeError' || err.name === 'TypeError') {
							// 发生了类型错误
							reject(new Error('发生了类型错误'));
						} else {
							// 其他未知错误
							reject(new Error('发生了未知错误'));
						}
					})
				});
			},
</script>

4.信令服务器

略(就是socket,里面写swich,不会私信,小额收费)文章来源地址https://www.toymoban.com/news/detail-826763.html

到了这里,关于uniapp开发WebRTC语音直播间支持app(android+IOS)和H5,并记录了所有踩得坑的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • UDP实现聊天直播间 chatroom

    1.memcmp() 函数 memcmp() 函数用于比较两个内存区域前 n 个字节的内容。 它接受三个参数: •const void *s1:指向第一个内存区域的指针。 •const void *s2:指向第二个内存区域的指针。 •size_t n:要比较的字节数。 返回值: memcmp() 函数返回一个整数值,表示两个内存区域内容的比

    2024年04月11日
    浏览(40)
  • 抖音直播间弹幕rpc学习

    目标url 随便找个直播间即可。 https://live.douyin.com/198986091107 接口分析 首先并没有在xhr下找到对应的接口 因为采用了websocket来传输信息。切换到ws即可看到 消息下,可以看到16进制的数据在源源不断地增加。 那么我们只要找到反序列化后的数据,再发送到本地的socket服务,就

    2023年04月22日
    浏览(74)
  • 抖音手机实景无人直播间怎么搭建?

    手机无人直播 已成为用户直播和商家直播带货的一项热门技术趋势,为消费者提供了全新的观看体验。无人直播,顾名思义,即通过无人直播软件或数字人来进行无人直播。这一技术的广泛应用,不仅为短视频渠道带来了更丰富的玩法,也为各种领域的商户提供了有效的带货

    2024年02月06日
    浏览(46)
  • python虚拟数字人直播间带货

    本开源项目名为“数字人控制器”。意为,本项目可以充当时下流行的虚拟人、虚拟主播、数字人,等仿人形数字形象的内核部分。 ​ 使用UE、C4D、DAZ、LIVE2D等三维引擎软件开发的数字形象可以与本“数字人控制器”对接,从而实现虚拟主播、数字导游、数字助手等。我们提

    2024年02月04日
    浏览(31)
  • GPT虚拟直播Demo系列(二)|无人直播间实现虚拟人回复粉丝

    虚拟人和数字人是人工智能技术在现实生活中的具体应用,它们可以为人们的生活和工作带来便利和创新。在直播间场景里,虚拟人和数字人可用于直播主播、智能客服、营销推广等。接入GPT的虚拟人像是加了超强buff,具备更强大的自然语言处理能力和智能对话能力,可以实

    2024年02月06日
    浏览(60)
  • 在抖音直播间,项目被资本割韭菜

    VC们也开始直播了。 想象中高高在上、西装革履的投资人对着镜头吆喝项目直投与连麦加群,颇有些魔幻现实的影子。但如果有心,在抖音上,时常可以发现诸如创投机构、XX投资人的直播,其中不乏已然有知名度的部分主体,这一趋势,在今年正在迅速蔓延。 论及缘由,在

    2024年02月16日
    浏览(42)
  • 销量破亿,董洁直播间凭何出圈?

    近期,“没有五位数走不出”的董洁直播间火了…… 纵观这几年各大平台直播带货,火已不是什么新鲜事,而为何董洁的直播能火上热搜呢? 本期,千瓜将解析董洁直播,同品牌方聊聊小红书直播的那些事儿。 董洁「种草式」直播 开启小红书直播新生态 据小红书官方数据

    2024年02月06日
    浏览(41)
  • 「GPT虚拟直播」实战篇|GPT接入虚拟人实现直播间弹幕回复

    ChatGPT和元宇宙都是当前数字化领域中非常热门的技术和应用。结合两者的优势和特点,可以探索出更多的应用场景和商业模式。例如,在元宇宙中使用ChatGPT进行自然语言交互,可以为用户提供更加智能化、个性化的服务和支持;在ChatGPT中使用元宇宙进行虚拟现实体验,可以

    2024年02月06日
    浏览(53)
  • 抖音直播VR全景3D立体元宇宙直播间搭建教程软件详细玩法教程

    标题:抖音直播VR全景3D立体元宇宙直播间搭建教程:详细玩法指南 简介: 本文将为您详细介绍抖音直播VR全景3D立体元宇宙直播间的搭建教程,并提供详细的玩法指南。通过了解的使用和相关软件的操作,帮助您在抖音直播平台上打造独特的虚拟现实直播体验。 第一

    2024年01月20日
    浏览(142)
  • uniapp - 超详细实现播放 svg / svga 格式动画组件插件,用于直播间赠送礼物特效动画或项目动画特效较多的应用(新手小白保姆级教程,提供插件+详细运行示例+使用文档+注意事项+格式说明)

    网上关于 uniapp 播放 svg / svga 格式动画的教程很乱,基本上全是 BUG 和各种不兼容,很难复制过来自己用。 本文实现了 在 uniapp 项目中(完美兼容 H5 / App / 微信小程序平台),播放 svg / svga 格式动画功能的详细介绍, 您只需要使用我提供的 “组件源码及插件”,放到项目中去

    2023年04月24日
    浏览(187)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包