uni-app webrtc 实现H5音视频通讯
Posted 范特西是只猫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了uni-app webrtc 实现H5音视频通讯相关的知识,希望对你有一定的参考价值。
文章目录
- 1. 写在前面
- 2. 项目需求 (安全帽视频对接)
- 3. 开始搞,uni-app 开发H5视频对接
- 3.1 html代码
- 3.2 js 代码(核心步骤)
- 3.2.1 根据接口获取安全帽在线的房间号,点击在线的安全帽列表,进入视频页面观看
- 3.2.2 进入房间后,首先 `uni.connectSocket` 创建初始化websocket连接
- 3.2.3 `uni.onSocketOpen` 打开连接,向服务端发送进入房间信息;并且创建心跳,每隔10s发送心跳信息。用于判断连接状态,如果断开,需要重新连接。
- 3.2.4 `uni.onSocketMessage` 进行服务端响应消息监听,
- 3.2.5 `connSignalServer` 进行连接音视频
- 3.2.6 `connFun` 进行监听服务端返回的值,然后进行一些逻辑操作。
- 3.2.7 `createPeerConnection` 创建本地流媒体链接
- 3.2.8 `call` 创建`createOffer` ,设置sdp,发送 message消息,发送sdp
- 3.2.10 设置拉流到video中
- 4. webrtc 媒体协商过程解释
1. 写在前面
之前本人一直没有做过webrtc相关的开发(进行实时语音对话或视频对话的),我上家公司的老板突然找到我,让我帮他做一个webrtc的模块功能。通过uni-app 去开发,然后打包到H5网页上进行音视频沟通。我主要是没有接触过,也不知道怎么去做,只是会uni-app,但是去对接webrtc 拿到手一脸雾水。不知道从何开始。后面各种百度,各种查资料,算是把这个功能搞出来了,现在想起来还是挺心酸的。
- uni-app websocket 开发参考 https://uniapp.dcloud.io/api/request/websocket.html
- 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
进入房间 ,服务的正常会返回joined
和otherjoin
(这个是根据前端和后端协商的,并不是固定的,只是我这里是这个。)
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中
- 这里使用原生的
document.getElementsByTagName('video')[0]
去获取,不使用refs,使用refs会报错- 这里使用
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 媒体协商流程
- 首先发起端要创建一个offer,并调用setLocalDescription设置本地的SDP
- 然后通过信令服务器将含有SDP的offer设置给对端
- 对端拿到此offer以后调用setRemoteDescription将此SDP信息保存
- 对端创建一个answer,并调用setLocalDescription设置本地的SDP
- 通过信令服务器将含有SDP的answer发送给发起端
- 发起端调用setRemoteDescription将此SDP信息保存
4.2 媒体协商方法
- createOffer
- createAnswer
- setLocalDescription
- setRemoteDescription
8┃音视频直播系统之 WebRTC 信令系统实现以及通讯核心并实现视频通话
〝 古人学问遗无力,少壮功夫老始成 〞
随着5G技术的发展,音视频直播领域发生了翻天覆地的变化,尤其是 2011 年 Google 推出 WebRTC 技术后,大大降低了音视频技术的门槛,你再也不必自己去实现回音消除算法了,也不用自己去实现各种音视频的编解码器了,更不必去考虑跨平台的问题了。如果这篇文章能给你带来一点帮助,希望给飞兔小哥哥一键三连,表示支持,谢谢各位小伙伴们。
目录
一、信令系统
-
信令系统主要用来进行信令的交换
-
在通信双方彼此连接、传输媒体数据之前,它们要通过信令服务器交换一些信息,如规范协商
-
若 A 与 B 要进行音视频通信,那么 A 要知道 B 已经上线了,同样,B 也要知道 A 在等着与它通信呢
-
只有双方都知道彼此存在,才能由一方向另一方发起音视频通信请求,并最终实现音视频通话
-
客户端代码如下:
-
第一步:首先弹出一个输入框,要求用户写入要加入的房间
-
第二步:通过
io.connect()
建立与服务端的连接 -
第三步:再根据
socket
返回的消息做不同的处理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>信令系统</title>
</head>
<body>
</body>
<script src="/socket.io/socket.io.js"></script>
<script>
var isInitiator;
// 弹出一个输入窗口
room = prompt('Enter room name:');
// 与服务端建立 socket 连接
const socket = io.connect();
// 如果房间不空,则发送 "create or join" 消息
if (room !== '')
console.log('Joining room ' + room);
socket.emit('create or join', room);
// 如果从服务端收到 "full" 消息
socket.on('full', (room) =>
console.log('Room ' + room + ' is full');
);
// 如果从服务端收到 "empty" 消息
socket.on('empty', (room) =>
isInitiator = true;
console.log('Room ' + room + ' is empty');
);
// 如果从服务端收到 “join" 消息
socket.on('join', (room) =>
console.log('Making request to join room ' + room);
console.log('You are the initiator!');
);
// 如果从服务端收到 “log" 消息
socket.on('log', (array) =>
console.log.apply(console, array);
);
</script>
</html>
-
服务端代码如下:
-
需要通过
npm install socket.io
安装socket模块 -
需要通过
npm install node-static
安装socket模块,使服务器具有发布静态文件的功能 -
服务端侦听 2022 这个端口,对不同的消息做相应的处理
const static = require('node-static');
const http = require('http');
const file = new (static.Server)();
const app = http.createServer(function (req, res)
file.serve(req, res);
).listen(2022);
// 侦听 2022
const io = require('socket.io').listen(app);
io.sockets.on('connection', (socket) =>
// convenience function to log server messages to the client
function log()
const array = ['>>> Message from server: '];
for (var i = 0; i < arguments.length; i++)
array.push(arguments[i]);
socket.emit('log', array);
socket.on('message', (message) =>
// 收到 message 时,进行广播
log('Got message:', message);
// for a real app, would be room only (not broadcast)
socket.broadcast.emit('message', message); // 在真实的应用中,应该只在房间内广播
);
socket.on('create or join', (room) =>
// 收到 “create or join” 消息
var clientsInRoom = io.sockets.adapter.rooms[room];
var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
log('Room ' + room + ' has ' + numClients + ' client(s)');
log('Request to create or join room ' + room);
if (numClients === 0)
// 如果房间里没人
socket.join(room);
// 发送 "created" 消息
socket.emit('created', room);
else if (numClients === 1)
// 如果房间里有一个人
io.sockets.in(room).emit('join', room);
socket.join(room);
// 发送 “joined”消息
socket.emit('joined', room);
else
// max two clients
// 发送 "full" 消息
socket.emit('full', room);
socket.emit('emit(): client ' + socket.id + ' joined room ' + room);
socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room);
);
);
二、RTCPeerConnection
-
RTCPeerConnection
类是在浏览器下使用 WebRTC 实现 1 对 1 实时互动音视频系统最核心的类 -
它是WebRTC传输音视频和交换数据的API
-
RTCPeerConnection
就与普通的 socket 一样,在通话的每一端都至少有一个RTCPeerConnection 对象。在 WebRTC 中它负责与各端建立连接,接收、发送音视频数据,并保障音视频的服务质量
三、实现视频通话
-
为连接的每个端创建一个
RTCPeerConnection
对象,并且给RTCPeerConnection
对象添加一个本地流,该流是从getUserMedia()
获取的 -
获取本地媒体描述信息,即 SDP 信息,并与对端进行交换
-
获得网络信息,即
Candidate(IP 地址和端口)
,并与远端进行交换
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实现视频通话</title>
</head>
<body>
<video id="localVideo" playsinline autoplay muted></video>
<video id="remoteVideo" playsinline autoplay></video>
<div class="box">
<button onclick="start()">Start</button>
<button onclick="call()">Call</button>
<button onclick="hangup()">Hang Up</button>
</div>
</body>
<script>
// 获取元素
var localVideo = document.getElementById('localVideo');
var remoteVideo = document.getElementById('remoteVideo');
// 定义全局变量
var localStream;
var pc1;
var pc2;
function start()
console.log('Requesting local stream');
// 开始采集音视频
navigator.mediaDevices.getUserMedia( audio: true, video: true )
.then(function (stream)
// 这个全局localStream是为了后面我们去添加流用的
localStream = stream;
// 兼容性监测
if (window.URL)
// 挂在数据在本地播放
localVideo.src = window.URL.createObjectURL(stream)
else
localVideo.srcObject = stream
)
.catch(function (e)
// 如果获取视频失败,在这里进行错误处理
console.dir(e);
alert(`getUserMedia() error: $e.message`);
);
function call()
// 创建offerOption, 指定创建本地的媒体的时候,都包括哪些信息
// 可以有视频流和音频流,因为我们这里没有采集音频所以offerToReceiveAudio是0
var offerOptions =
offerToReceiveAudio: 0,
offerToReceiveVideo: 1
// 这里的 RTCPeerConnection 可以有可选参数, 进行一些网络传输的配置
// 由于是我们在本机内进行传输,所以在这里我们就不需要设置参数, 所以它这里就会使用本机host类型的candidate
pc1 = new RTCPeerConnection();
pc1.onicecandidate = (e) =>
console.log('pc1 ICE candidate:', e.candidate);
// 我们A调用者收到candidate之后,它会将这个candidate发送给这个信令服务器
// 那么信令服务器会中转到这个B端,那么这个B端会调用这个AddIceCandidate这个方法,将它存到对端的candidate List里去
// 所以整个过程就是A拿到它所有的可行的通路然后都交给B,B形成一个列表
// 那么B所以可行的通路又交给A,A拿到它的可行列表,然后双方进行这个连通性检测
// 那么如果通过之后那就可以传数据了,就是这样一个过程
// 所以我们收到这个candidate之后就要交给对方去处理,所以pc1要调用pc2的这个
// 因为是本机这里就没有信令了,假设信令被传回来了,这时候就给了pc2
// pc2收到这个candidate之后就调用addIceCandidate方法,传入的参数就是e.candidate
pc2.addIceCandidate(e.candidate)
.catch(function (e)
console.log("Failed to call getUserMedia", e);
);
pc1.iceconnectionstatechange = (e) =>
console.log(`pc1 ICE state: $pc.iceConnectionState`);
console.log('ICE state change event: ', e);
// 创建一个pc2这样我们就创建了两个连接
pc2 = new RTCPeerConnection();
// 对于pc2也是同样道理,那它就交给p1
pc2.onicecandidate = (e) =>
console.log('pc2 ICE candidate:', e.candidate);
// 所以它就调用pc1.addIceCandidate
pc1.addIceCandidate(e.candidate)
.catch(function (e)
console.log("Failed to call getUserMedia", e);
);
pc2.iceconnectionstatechange = (e) =>
console.log(`pc2 ICE state: $pc.iceConnectionState`);
console.log('ICE state change event: ', e);
// pc2是被调用方,被调用方是接收数据的,所以对于pc2它还有个ontrack事件
// 当双方通讯连接之后,当有流从对端过来的时候,会触发这个onTrack事件
pc2.ontrack = gotRemoteStream;
// 将本地采集的数据添加到第一添加到第一个pc1 = new RTCPeerConnection()中去
// 这样在创建媒体协商的时候才知道我们有哪些媒体数据,这个顺序不能乱,必须要先添加媒体数据再做后面的逻辑
// 另外不能先做媒体协商然后在添加数据,因为你先做媒体协商的时候它知道你这里没有数据那么在媒体协商的时候它就没有媒体流
// 就是说在协商的时候它知道你是没有的,那么它在底层就不设置这些接收信息发收器,那么这个时候即使你后面设置了媒体流传给这个PeerConnection,它也不会进行传输的,所以我们要先添加流
// 添加流也比较简单,通过localStream调用getTracks就能调用到所有的轨道(音频轨/视频轨)
// 那对于每个轨道我们添加进去就完了,也就是forEach遍历进去,每次循环都能拿到一个track
// 当我们拿到这个track之后直接调用pc1.addTrack添加就好了,第一个参数就是track,第二个参数就是这个track所在的流localStream
// 这样就将本地所采集的音视频流添加到了pc1 这个PeerConnection
localStream.getTracks().forEach((track) =>
pc1.addTrack(track, localStream);
);
// 那么这个时候我们就可以去创建这个pc1去媒体协商了
// 媒体协商第一步就是创建createOffer
pc1.createOffer(offerOptions)
.then(function (desc)
// 当我们拿到这个描述信息之后呢,还是回到我们当时协商的逻辑
// 对于A来说它首先创建Offer,创建Offer之后它会调用setLocalDescription
// 将它设置到这个PeerConnection当中去,那么这个时候它会触发底层的ICE的收集candidate的这个动作
// 所以这里要调用pc1.setLocalDescription这个时候处理完了它就会收集candidate
// 这个处理完了之后按照正常逻辑它应该send desc to signal到信令服务器
pc1.setLocalDescription(desc);
// 到了信令服务器之后,信令服务器会发给第二个人
// 所以第二个人就会receive
// 所以第二个人收到desc之后呢首先pc2要调用setRemoteDescription,这时候将desc设置成它的远端
pc2.setRemoteDescription(desc);
// 设成远端之后, pc2就要调用createAnswer
pc2.createAnswer().then(function (desc)
// 当远端它得到这个Answer之后,它也要设置它的setLocalDescription
// 当它调用了setLocalDescription之后它也开始收集candidate了
pc2.setLocalDescription(desc);
// 完了之后它去进行send desc to signal与pc1进行交换,pc1会接收recieve desc from signal
// 那么收到之后他就会设置这个pc1的setRemoteDescription
// 那么经过这样一个步骤整个协商就完成了
// 当所有协商完成之后,这些底层对candidate就收集完成了
// 收集完了进行交换形成对方的列表然后进行连接检测
// 连接检测完了之后就开始真正的数据发送过来了
pc1.setRemoteDescription(desc);
)
.catch(function (e)
console.log("Failed to call getUserMedia", e);
);
)
.catch(function (e)
console.log("Failed to call getUserMedia", e);
);
// 当发送ontrack的时候也就是数据通过的时候, 将远端的音视频流传给了remoteVideo
function gotRemoteStream(e)
if (remoteVideo.srcObject !== e.streams[0])
remoteVideo.srcObject = e.streams[0];
// 挂断,将pc1和pc2分别关闭
function hangup()
console.log('Ending call');
pc1.close();
pc2.close();
pc1 = null;
pc2 = null;
</script>
</html>
四、视频通话流程详解
-
视频通话本是不同的端与端连接,上面的代码在同一个浏览器中模拟多端连接的情况,可以通过开两个标签页,来模拟pc1端和pc2端
-
所以大家会看到两个视频是一摸一样的,但是它的整个底层都是从本机自己IO的那个逻辑网卡转过来的
-
当调用 call 的时候就会调用双方的
RTCPeerConnection
-
当这个两个
PeerConnection
创建完成之后,它们会作协商处理 -
协商处理完成之后进行
Candidate
采集,也就是说有效地址的采集 -
采集完了之后进行交换,然后形成这个
Candidate pair
再进行排序 -
然后再进行连接性检测,最终找到最有效的那个链路
-
之后就将 localVideo 展示的这个数据通过
PeerConnection
传送到另一端 -
另一端收集到数据之后会触发
onAddStream
或者onTrack
就是说明我收到数据了,那当收到这个事件之后 -
我们再将它设置到这个
remoteVideo
里面去 -
这样远端的这个
video
就展示出来了,显示出我们本地采集的数据了
以上是关于uni-app webrtc 实现H5音视频通讯的主要内容,如果未能解决你的问题,请参考以下文章
8┃音视频直播系统之 WebRTC 信令系统实现以及通讯核心并实现视频通话