1. RTCPeerConnection
在开始一对一通话实战前,先看下RTCPeerConnection的定义及可选参数; RTCPeerConnection接口代表一个由本地计算机到远端的WebRTC连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。 其接口的定义如下:
declare var RTCPeerConnection: {
prototype: RTCPeerConnection;
new(configuration?: RTCConfiguration): RTCPeerConnection;
generateCertificate(keygenAlgorithm: AlgorithmIdentifier): Promise<RTCCertificate>;
};
注意其中有一个可选参数RTCConfiguration , 在文档中定义如下;
interface RTCConfiguration {
bundlePolicy?: RTCBundlePolicy;
certificates?: RTCCertificate[];
iceCandidatePoolSize?: number;
iceServers?: RTCIceServer[];
iceTransportPolicy?: RTCIceTransportPolicy;
rtcpMuxPolicy?: RTCRtcpMuxPolicy;
}
-
iceServers,由多个RTCIceServer组成需要填入stun货turn服务的地址; -
iceTransportPolicy :ice的传输策略,默认值是all允许考虑所有候选者,值有"all",“public” 已弃用 ,“relay” 只收集中继候选者; -
rtcpMuxPolicy:收集 ICE 候选时是否使用的 RTCP 多路复用策略。值有 'negotiate’和 ‘require’; -
bundlePolicy: ‘balanced’、‘max-compat’和’max-bundle’;各个含义如下:
一般的使用如下:
const config = {
bundlePolicy: 'balanced',
iceTransportPolicy: "all",
rtcpMuxPolicy : 'negotiate',
iceServers: [
{
urls: "turn:www.lymggylove.top:3478",
username: "lym",
credential: "123456"
}
]
};
2. 实战一对一视频通话
主要以js为例,做简单的demo展示,本地设备获取的逻辑之前的文章有介绍,这里修改如下:
- 新增属性
var socket;
var room;
var localStream;
var isGet = false;
var isStartRecored = false;
var isSetRemote = false;
let peerconnetion = null;
var isOffer = true;
var recvSdp = {
sdp: null,
type: null
};
var cacheCandidateMsg = [];
var selfid = '';
其中localstream做成全局的是因为其他地方需要使用,比如录制视频的时候,赋值代码如下:
function startWebCam() {
return new Promise((resolve, reject) => {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
document.write('当前浏览器不支持 getUserMedia()!!!!/n');
return reject('当前浏览器不支持 getUserMedia()!!!!/n');
} else {
const videoDeviceIds = videoSource.value;
const audioDeviceIds = audioSource.value;
var constraints = {
audio: {
noiseSuppression: true,
echoCancellation: true,
deviceId: videoDeviceIds ? videoDeviceIds : undefined
},
video: {
width: 320,
height: 240,
frameRate: { ideal: 10, max: 15 },
deviceId: audioDeviceIds ? audioDeviceIds : undefined
},
};
navigator.mediaDevices.getUserMedia(constraints).then(function (mediaStream) {
localStream = mediaStream;
const videoTrack = mediaStream.getVideoTracks()[0];
const videoConstraints = videoTrack.getSettings();
showDiv.textContent = JSON.stringify(videoConstraints, null, 2);
videoPlayer.srcObject = mediaStream;
videoPlayer.onloadedmetadata = function (e) {
videoPlayer.play();
};
return resolve(mediaStream);
}).catch((err) => {
return reject(err);
console.log(err.name + ": " + err.message);
});
}
});
}
这里转成Promise写法是为了后面使用asyn/await方便,同样的获取设备列表修改如下:
function getUserMedia() {
return new Promise((resolve, reject) => {
navigator.mediaDevices.enumerateDevices().then((devices) => {
if (!isGet) {
isGet = true;
devices.forEach((devInfo) => {
var option = document.createElement('option');
option.text = devInfo.label;
option.value = devInfo.deviceId;
if (devInfo.kind === 'audioinput') {
audioSource.appendChild(option);
} else if (devInfo.kind === 'audiooutput') {
audioOutput.appendChild(option);
} else if (devInfo.kind === 'videoinput') {
videoSource.appendChild(option);
}
});
}
resolve(devices);
});
});
}
- peer的创建和基本使用
上一节已经介绍过webrtc中peerconnetcion的基本配置方法,这里看下其使用:
async function InitPeerconnect() {
console.log('开始初始化摄像头。。。。');
await startWebCam();
await getUserMedia();
console.log('结束初始化摄像头。。。。');
const config = {
bundlePolicy: 'balanced',
iceTransportPolicy: "all",
rtcpMuxPolicy: 'negotiate',
iceServers: [
{
urls: "turn:www.lymggylove.top:3478",
username: "lym",
credential: "123456"
}
]
};
peerconnetion = new RTCPeerConnection(config);
peerconnetion.ontrack = (ev) => {
if (ev.streams && ev.streams[0]) {
remoteVideoPlayer.srcObject = ev.streams[0];
} else {
const inboundStream = new MediaStream();
inboundStream.addTrack(ev.track);
remoteVideoPlayer.srcObject = inboundStream;
}
};
peerconnetion.onicecandidate = async (ev) => {
console.log('=======>' + JSON.stringify(ev.candidate));
if (socket) {
await socket.emit('message', room, {
type: 2,
candidate: ev.candidate
});
}
};
peerconnetion.oniceconnectionstatechange = (ev)=>{
outputArea.scrollTop = outputArea.scrollHeight;
outputArea.value = outputArea.value + JSON.stringify(peerconnetion.iceConnectionState) + '\r';
};
for (const track of localStream.getTracks()) {
peerconnetion.addTrack(track);
}
if (isOffer) {
const offerOption = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
const offerSdp = await peerconnetion.createOffer(offerOption);
if (socket) {
await socket.emit('message', room, {
type: 0,
sdp: offerSdp
});
}
const errLocalDescription = await peerconnetion.setLocalDescription(offerSdp);
if (errLocalDescription) {
console.error('setLocalDescription err :' + JSON.stringify(offerSdp));
return;
}
} else {
const answerOption = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
const errsetRemoteDescription = await peerconnetion.setRemoteDescription(recvSdp);
if (errsetRemoteDescription) {
console.error('answer setRemoteDescription err :' + JSON.stringify(recvSdp));
return;
}
isSetRemote = true;
const answerSDP = await peerconnetion.createAnswer(answerOption);
if (socket) {
await socket.emit('message', room, {
type: 1,
sdp: answerSDP
});
}
const setLocalDescriptionErr = await peerconnetion.setLocalDescription(answerSDP);
addcandidateFUN();
}
}
函数开始使用asyc/await的语法糖去获取本地的媒体流,这样可以使的流程看起来更简洁,
- 主要是设置RTCPeerConnection的打洞服务地址,其他的可以忽略使用默认的就可以,这里加上是为了演示如何配置;
- 设置peerconnetion的一些回调监听函数,其中
onicecandidate 回调ice打洞地址信息用于通过信令服务发送个对端。oniceconnectionstatechange 是ice打洞的状态信息,可以输出到控制台,这里为了方便直接输出到textview上,显示的信息如下: 上面图中红线标记的就是ice状态的一部分; - 将locaStream中的所有track添加的peercnnection中;
- 判断如果是主角一方就调用create offer生成本地sdp信息,这时候可以使用socketio发送给对端,接着调用set local使用此sdp;
- 如果是接收放先把对方的offer sdp调用setremotet方法设置给peerconnection,接着调用create answer生成自己的answersdp,使用socketio将消息发送给对端;
需要注意的是candidate的收集在setloca后就开始;
- 服务消息的处理
对服务的主要消息是answer/offer sdp的接收,以及candidate的处理,代码如下:
socket.on('message', (room, id, data) => {
if (id === selfid) {
return;
}
const type = data.type;
switch (type) {
case 0: {
isOffer = false;
recvSdp = data.sdp;
InitPeerconnect();
}
break;
case 1: {
peerconnetion.setRemoteDescription(data.sdp);
isSetRemote = true;
addcandidateFUN();
}
break;
case 2: {
if (isSetRemote == true) {
outputArea.scrollTop = outputArea.scrollHeight;
} else {
cacheCandidateMsg.push(data.candidate);
addcandidateFUN();
}
outputArea.value = outputArea.value + JSON.stringify(data.candidate) + '\r';
peerconnetion.addIceCandidate(data.candidate);
}
break;
default:
break;
}
});
这里发送的消息每一个都有一个type用于表示消息的类型,客户端在收到后按照不同类型处理。如果是offer的消息,这时候被叫就可以开始初始化peer及接口调用;
- 如果是answer 消息,直接调用setremote,将answersdp设置给peer;
- candidate的处理比较麻烦些因为需要在setremote之后设置;所以如果peerconnection的set remote方法还没调用就需要缓存起来,等调用后再调用,这里封装成了方法:
function addcandidateFUN(){
cacheCandidateMsg.forEach((item, index, arr)=> {
peerconnetion.addIceCandidate(item) });
cacheCandidateMsg = [];
}
循环去调用addIceCandidate方法,把缓存的所有candidate设置给peer;然后清空缓存; 5 结束方法如下:
function peerCloseFun () {
isStartRecored = false;
for (const track of localStream.getTracks()) {
track.stop();
}
peerconnetion.close();
localStream = null;
cacheCandidateMsg = [];
videoPlayer.srcObject = null;
remoteVideoPlayer.srcObject = null;
isSetRemote = false;
isOffer = true;
recvSdp = null;
inputArea.value = '';
peerconnetion = null;
}
释放的主要是把所有的本地流停掉,然后调用peerconnection的stop方法;接着释放全局变量为下一次通话做准备; 完整代码地址:WebRTCDemo js
测试网页:demo效果展示
|