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

avatar
作者
筋斗云
阅读量:0

一、效果图

二、主要功能

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

2. 查询所有直播间列表

3.加入房间

4.申请上位

5.麦克风控制

6.声音控制

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

8.退出房间

三、原理

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

2.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,不会私信,小额收费)

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!