1. 写在前面

之前本人一直没有做过webrtc相关的开发(进行实时语音对话或视频对话的),我上家公司的老板突然找到我,让我帮他做一个webrtc的模块功能。通过uni-app 去开发,然后打包到H5网页上进行音视频沟通。我主要是没有接触过,也不知道怎么去做,只是会uni-app,但是去对接webrtc 拿到手一脸雾水。不知道从何开始。后面各种百度,各种查资料,算是把这个功能搞出来了,现在想起来还是挺心酸的。

  1. uni-app websocket 开发参考 https://uniapp.dcloud.io/api/request/websocket.html
  2. webrtc 开发参考 https://webrtc.github.io/samples/

2. 项目需求 (安全帽视频对接)

安全帽是一个特制的帽子,不同与普通的安全帽,而是一个有电源开关,有摄像头,有开灯光的帽子。
在这里插入图片描述

通俗点就是,安全帽那边是一个工地的工作人员(A端),带上帽子进行作业。遇到困难,需要办公室高级技术人员(B端)去指挥工人作业操作,安全帽A端是无法看到B端,但是B端可以通过在H5网页上,然后进行观A端看那边的作业情况,进行指挥。

2.1 完成效果

请添加图片描述

3. 开始搞,uni-app 开发H5视频对接

3.1 html代码

就是声明一个video标签,进行视频播放使用,(关键的,那几个按钮的不重要这里不写了)

<template>
	<view class="container">
		<div class="video-cont">
			<video id="remoteVideo" :muted="muted" autoplay></video>
		</div>
	</view>
</template>

3.2 js 代码(核心步骤)

3.2.1 根据接口获取安全帽在线的房间号,点击在线的安全帽列表,进入视频页面观看

在这里插入图片描述
data 数据

return {
	hatid: '', //房间号
	ws: null,//ws
	socket: null,//socket
	state: 'init',//状态
	pc: null,
	localStream: null,
	socketOpen: false,
	muted: false, //是否静音
};

进入页面获取列表传的 hatid ,调用 initWebSocket 方法

onLoad(option) {
	if (option.hat_id) {
		this.hatid = option.hat_id
	}
},
onReady() {
	this.initWebSocket(this.hatid); //连接WebSocket
},
3.2.2 进入房间后,首先 uni.connectSocket 创建初始化websocket连接
this.ws = uni.connectSocket({
	url: "wss://rtc.xxxxxxx.cn/ws",//你自己的地址
	success: (res) => {
		console.log("WebSocket服务连接成功!");
	},
	fail: (err) => {
		uni.showToast({
			title: JSON.stringify(err),
			icon: 'error'
		});
	}
});
3.2.3 uni.onSocketOpen 打开连接,向服务端发送进入房间信息;并且创建心跳,每隔10s发送心跳信息。用于判断连接状态,如果断开,需要重新连接。
// 2. 连接打开
uni.onSocketOpen((res) => {
	this.socketOpen = true
	// 打开后发送一条消息
	uni.sendSocketMessage({
		data: `{"isHat":"N" ,"type":"on_line" ,"hatId":${this.hatid} }`
	});
	// 10s 发送一次心跳
	this.heartbeatInterval = setInterval(() => {
		// console.log("轮询监听WebSocket状态:" + this.ws.readyState)
		// CONNECTING 0  OPEN 1 打卡状态  CLOSING 2  CLOSED 3 断开状态
		if (this.ws.readyState === this.ws.OPEN) {
			// 打开状态
			uni.sendSocketMessage({
				data: "keep alive admin:" + 'xiehao' + " connect:" + this.hatid,
			});
		} else if (this.ws.readyState === this.ws.CLOSED) {
			// 判断如果断开,需要重新链接
			this.initWebSocket(this.hatid)
		} else if (this.ws.readyState === this.ws.CLOSING || this.ws.readyState === this.ws
			.CONNECTING) {
			//不用管
		}
	}, 10000)
});

可以看到,我们已经创建了连接,并且在发送心跳信息,服务的响应消息为的 allow
在这里插入图片描述

3.2.4 uni.onSocketMessage 进行服务端响应消息监听,

这里判断如果返回消息为full 则是房间已满,无法进行查询通话
如果返回 allow 则没有人进入房间,允许进入房间进行通话,然后进入方法 connSignalServer 连接音视频

uni.onSocketMessage((res) => {
	var msg = res.data;
	if (msg.indexOf("full") !== -1) {
		uni.showToast({
			title: '当前安全帽有人在查看,您暂时无法查看!',
			icon: 'error'
		});
		this.state = 'full';
	} else if (msg.indexOf("allow") !== -1) {
		console.log("准备连接音视频。。。。。。")
		this.connSignalServer(); //连接音视频
	}
});
3.2.5 connSignalServer 进行连接音视频

navigator.mediaDevices 进行媒体兼容判断,如果浏览器支持播放,则进入connFun 方法
这里涉及到一个开发问题,则是在本地开发环境,浏览器访问需要使用https或者localhost进行访问,不能使用http进行访问,否则会走不下去,报错进入handleError 方法。

connSignalServer() {
	// 开启本地视频 
	if (!navigator.mediaDevices ||
		!navigator.mediaDevices.getUserMedia) {
		alert("getUserMedia is not supported!")
		return;
	} else {
		//1 ===============配置音视频参数===============
		let constraints = {
			video: false, //先设置为false进行调试
			audio: true
		}
		navigator.mediaDevices.getUserMedia(constraints)
			.then(this.getMediaStream)
			.catch(this.handleError)
	}
},
getMediaStream(stream) {
	this.localStream = stream;
	//这个函数的调用时机特别重要 一定要在getMediaStream之后再调用,否则会出现绑定失败的情况
	this.connFun();
},
handleError(err) {
	if (err) console.error("getUserMedia  error:", err);
}
3.2.6 connFun 进行监听服务端返回的值,然后进行一些逻辑操作。

1 创建 socket 连接,emit 发送 join 进入房间 ,服务的正常会返回 joinedotherjoin (这个是根据前端和后端协商的,并不是固定的,只是我这里是这个。)
2 监听返回 joined 进入 createPeerConnection 方法

3 监听返回 otherjoin 进入 call 进行媒体协商

import io from './js/socket.io.js'
connFun() {
	this.socket = io('https://rtc.xxxxxxx.cn/');
	this.socket.on('joined', (roomid, id) => {
		this.state = 'joined';
		this.createPeerConnection()
	});
	this.socket.on('otherjoin', (roomid, id) => {
		this.state = 'joined_conn';
		//媒体协商
		this.call();
	});
	this.socket.on('message', (roomid, id, data) => {
		//媒体协商
		if (data) {
			if (data.type === 'offer') {
				this.pc.setRemoteDescription(new RTCSessionDescription(data));
				this.pc.createAnswer()
					.then(this.getAnswer)
					.catch(this.handleAnswerError);
			} else if (data.type === 'answer') {
				this.pc.setRemoteDescription(new RTCSessionDescription(data));
			} else if (data.type === 'candidate') {
				var candidate = new RTCIceCandidate({
					sdpMLineIndex: data.label,
					candidate: data.candidate
				});
			} else {
				console.error('the message is invalid!', data)
			}
		}
	});
	if (this.socket.emit()) {
		this.socket.emit('join', this.hatid);
	}
	return;
},
getAnswer(desc) {
	this.pc.setLocalDescription(desc);
	this.socket.emit('message', this.hatid, desc);
},
handleAnswerError(err) {
	console.error('Failed to get Answer!', JSON.stringify(err));
},
3.2.7 createPeerConnection 创建本地流媒体链接
createPeerConnection() {
	if (!this.pc) {
		this.pc = new RTCPeerConnection({
			'iceServers': [{
				'urls': 'turn:175.178.21.191:xxxx',
				'credential': 'xxxxxxxx',
				'username': 'xxxx'
			}],
		});
		this.pc.onicecandidate = (e) => {
			if (e.candidate) {
				this.socket.emit('message', this.hatid, {
					type: 'candidate',
					label: e.candidate.sdpMLineIndex,
					id: e.candidate.sdpMid,
					candidate: e.candidate.candidate
				});
			}
		}
		this.pc.ontrack = (e) => {
			if (e.streams.length > 0) {
				let videoElement = document.getElementsByTagName('video')[0]
				videoElement.srcObject = e.streams[0];
			}
		}
	}
	if (this.pc === null || this.pc === undefined) {
		console.log('pc is null or undefined!');
		return;
	}
	if (this.localStream === null || this.localStream === undefined) {
		console.log('localStream is null or undefined!');
		// return;
	}
	if (this.localStream) {
		this.localStream.getTracks().forEach((track) => {
			this.pc.addTrack(track, this.localStream);
		});
	}
},
3.2.8 call 创建createOffer ,设置sdp,发送 message消息,发送sdp

在3.2.6 中 的方法,已经监听了服务的返回 message 消息

call() {
	if (this.state === 'joined_conn') {
		if (this.pc) {
			var options = {
				offerToReceiveAudio: 1,
				offerToReceiveVideo: 1
			}
			this.pc.createOffer(options)
				.then(this.getOffer)
				.catch(this.handleOfferError);
		}
	}
},
getOffer(desc) {
	this.pc.setLocalDescription(desc);
	if (this.socket) {
		this.socket.emit('message', this.hatid, desc);
	}
},
handleOfferError(err) {
	console.error('Failed to get Offer!', JSON.stringify(err));
},
3.2.10 设置拉流到video中
  1. 这里使用原生的 document.getElementsByTagName('video')[0] 去获取,不使用refs,使用refs会报错
  2. 这里使用srcObject 不能使用src去设置
this.pc.ontrack = (e) => {
	if(e.streams.length > 0) {
		let videoElement = document.getElementsByTagName('video')[0]
		videoElement.srcObject = e.streams[0];
	}
}

4. webrtc 媒体协商过程解释

媒体协商是为了保证交互双方通过交换信息来保证交互的正常进行,比如A用的是H264编码,通过协商告知B,B来判断自己是否可以进行相应的数据解析来确定是否可以进行交互通信。WebRTC默认情况下使用的V8引擎。
在这里插入图片描述

4.1 媒体协商流程

  1. 首先发起端要创建一个offer,并调用setLocalDescription设置本地的SDP
  2. 然后通过信令服务器将含有SDP的offer设置给对端
  3. 对端拿到此offer以后调用setRemoteDescription将此SDP信息保存
  4. 对端创建一个answer,并调用setLocalDescription设置本地的SDP
  5. 通过信令服务器将含有SDP的answer发送给发起端
  6. 发起端调用setRemoteDescription将此SDP信息保存

4.2 媒体协商方法

  1. createOffer
  2. createAnswer
  3. setLocalDescription
  4. setRemoteDescription
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐