WebRTC系列--js 实现一对一通话

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',
    // certificates?: RTCCertificate[];
   // iceCandidatePoolSize?: number;
   iceTransportPolicy: "all",//  public relay
   rtcpMuxPolicy : 'negotiate',
   iceServers: [
            	{
	              urls: "turn:www.lymggylove.top:3478",
	              username: "lym",
	              credential: "123456"
                }
            ]
     };

2. 实战一对一视频通话

主要以js为例,做简单的demo展示,本地设备获取的逻辑之前的文章有介绍,这里修改如下:

  1. 新增属性
// 客户端的socketio ,用于后续发送信令
var socket;
// 房间ID 后续的消息都要携带这个ID
var room;
// 本地流 mediastream
var localStream;

// 防止重复去获取设备列表
var isGet = false;
var isStartRecored = false;
// 记录是不是已经调用set remote接口,因为addicecandidate的调用,要在set remote之后
var isSetRemote = false;
// 全局的RTCPeerconnection对象
let peerconnetion = null;
// 是不是主叫
var isOffer = true;
// sdp对象。主要是主角方发送的offer sdp的缓存;
var recvSdp = {
    sdp: null,
    type: null
};
//消息队列用于存放 candidate消息
var cacheCandidateMsg = [];
//记录socket连接服务后返回的自己当前客户端的ID信息
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 {

            // 想要获取一个最接近 1280x720 的相机分辨率
            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;
                // 获取视频的track
                const videoTrack = mediaStream.getVideoTracks()[0];
                //拿到video的所有约束
                const videoConstraints = videoTrack.getSettings();
                // 转成jsonstring显示到div标签上
                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);
        });
    });
}
  1. peer的创建和基本使用
    上一节已经介绍过webrtc中peerconnetcion的基本配置方法,这里看下其使用:
  async function InitPeerconnect() {
    console.log('开始初始化摄像头。。。。');
    await startWebCam();
    await getUserMedia();
    console.log('结束初始化摄像头。。。。');
    const config = {
        bundlePolicy: 'balanced',
        // certificates?: RTCCertificate[];
        // iceCandidatePoolSize?: number;
        iceTransportPolicy: "all",//  public relay
        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;
        }
        // if (trackEvent.track.kind === 'video') {
        //     remoteVideoPlayer.srcObject = trackEvent[0];
        // }
    };
    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,
        };
        // RTCSessionDescriptionInit init = 
        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后就开始;
  1. 服务消息的处理
    对服务的主要消息是answer/offer sdp的接收,以及candidate的处理,代码如下:
 socket.on('message', (room, id, data) => {
        if (id === selfid) {
            return;
        }
        const type = data.type;
        switch (type) {
            case 0: {// offer
                isOffer = false;
                recvSdp = data.sdp;
                InitPeerconnect();
            }
                break;
            case 1: {// answer
                peerconnetion.setRemoteDescription(data.sdp);
                isSetRemote = true;
                addcandidateFUN();
            }
                break;
            case 2: {// candidate
                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;
        }
        // outputArea.scrollTop = outputArea.scrollHeight;//窗口总是显示最后的内容
        // outputArea.value = outputArea.value + data + '\r';
    });

这里发送的消息每一个都有一个type用于表示消息的类型,客户端在收到后按照不同类型处理。如果是offer的消息,这时候被叫就可以开始初始化peer及接口调用;

  • 如果是answer 消息,直接调用setremote,将answersdp设置给peer;
  • candidate的处理比较麻烦些因为需要在setremote之后设置;所以如果peerconnection的set remote方法还没调用就需要缓存起来,等调用后再调用,这里封装成了方法:
function addcandidateFUN(){
    cacheCandidateMsg.forEach((item, index, arr)=> {
        peerconnetion.addIceCandidate(item) }); // undefined
        cacheCandidateMsg = [];
}

循环去调用addIceCandidate方法,把缓存的所有candidate设置给peer;然后清空缓存;
5 结束方法如下:

function peerCloseFun ()  {
    isStartRecored = false;
    for (const track of localStream.getTracks()) {
        // peerconnetion.removeTrack(track);
        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效果展示

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要使用 webRTC 进行一对一的视频通话,需要以下步骤: 1. 建立本地媒体流对象 使用 getUserMedia() 方法获取本地视频和音频流对象。 ```javascript navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(function(localStream) { // localStream 是本地媒体流对象 }) .catch(function(error) { console.log(error); }); ``` 2. 建立 peer connection 对象 使用 RTCPeerConnection() 方法创建 peer connection 对象,并将本地媒体流对象添加到 peer connection 中。 ```javascript var pc = new RTCPeerConnection(); localStream.getTracks().forEach(function(track) { pc.addTrack(track, localStream); }); ``` 3. 建立远程媒体流对象 通过 peer connection 对象,监听远程的媒体流对象。 ```javascript pc.addEventListener('track', function(event) { // event.streams 是一个媒体流对象数组 var remoteStream = event.streams[0]; }); ``` 4. 发送 offer 和 answer 通过 peer connection 对象,发送 offer 和 answer 信令,建立连接。 ```javascript pc.createOffer() .then(function(offer) { return pc.setLocalDescription(offer); }) .then(function() { // 发送 offer 信令 }) .catch(function(error) { console.log(error); }); // 接收到 offer 信令后,回复 answer 信令 pc.setRemoteDescription(offer) .then(function() { return pc.createAnswer(); }) .then(function(answer) { return pc.setLocalDescription(answer); }) .then(function() { // 发送 answer 信令 }) .catch(function(error) { console.log(error); }); // 接收到 answer 信令后,设置远程描述 pc.setRemoteDescription(answer) .catch(function(error) { console.log(error); }); ``` 5. 连接成功后,进行视频通话 当连接成功后,就可以在页面中显示本地视频和远程视频,并进行视频通话了。 ```javascript // 添加本地视频和远程视频 var localVideo = document.getElementById('localVideo'); var remoteVideo = document.getElementById('remoteVideo'); localVideo.srcObject = localStream; remoteVideo.srcObject = remoteStream; ``` 完整的一对一视频通话示例代码请参考:https://webrtc.github.io/samples/src/content/peerconnection/pc1/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简简单单lym

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值