zl程序教程

您现在的位置是:首页 >  前端

当前栏目

WebRTC本地实现 - WebRTC通信实现(Web端)

Web通信 实现 本地 Webrtc
2023-09-27 14:27:27 时间

操作系统准备

  • 需要准备好摄像头,由于本电脑没有摄像头,这里使用虚拟摄像头(VCam)来代替真实摄像头

效果展示

  • 本地台式机使用VCam虚拟摄像头来模拟视频流
  • 远程笔记本访问同一链接,从而进行通信

 

通信效果展示

代码提取

WebRTC通信实现明细

  • 该文章代码是在 WebRTC本地实现 - Vue与Socket IO通信项目搭建与测试(https)基础上进行更改操作
  • 原文章修改的代码内容如下【可查看本文章: 2 - 流程代码】
    1. 删除原来的发送信息的按钮和test方法
    2. 增加方法 sendMessage

 

// 发送消息给 Socket IO 服务器
sendMessage(msg) {
    this.$socket.emit('message', sendMessage)
}

代码编写流程

  • 客户Peer - A 和 Peer - B进行通信,有可能是A先发起 或者 B先发起,所以代码发起端跟接收端都得写
  1. Connect Signal Server:2端 连接到信令服务器,也就是之前与Socket IO进行通信过程
  • 流程图片

 

1 - Connect Signal Server

  • 流程说明
    A用户连接到Sinal Server(也就是Socket IO 服务器)
    B用户连接到Sinal Server(也就是Socket IO 服务器)
  1. getUserMedia:2端 获取本地媒体流
  • 流程图片

 

2 - getUserMedia

  • 流程说明
    A用户本地的媒体流,并设置localStream
    B用户本地的媒体流,并设置localStream

  • 流程代码(this.$lib.logInfo等可直接替换为console.log)

 

<style>
    video {
        max-width: 100%;
        width: 320px;
        border: 1px solid;
    }
</style>

<template>
    <el-row>
        <el-col :span="12">
            <h1>本地视频流</h1>
            <video ref="localVideo" autoplay muted playsinline></video>
        </el-col>
        <el-col :span="12">
            <h1>远程视频流</h1>
            <video ref="remoteVideo" autoplay playsinline></video>
        </el-col>
    </el-row>
</template>

<script>
    export default {
        data() {
            return {
                // 本地媒体流
                localStream: null,
                // 是否为创始者
                isInitiator: false
            }
        },
        mounted() {
            // Socket IO 存在 && 未连接到服务器
            if (this.$socket != null && !this.$socket.connected) {
                this.$socket.connect();
            }

            window.onload = () => {
                // 获取本地用户媒体流
                this.getUserMedia();
            };
        },
        methods: {
            // 获取本地用户媒体流
            getUserMedia() {
                // navigator.mediaDevices.getUserMedia 3个工作域:http://localhost/ 、HTTPS://或file://,否则会是undefined
                navigator.mediaDevices
                    .getUserMedia({
                        audio: false,
                        video: true
                    })
                    .then((stream) => {
                        this.$lib.logInfo("获取本地用户媒体流");
                        // 赋值本地媒体流
                        this.localStream = stream;
                        // 在浏览器中进行展示            
                        this.$refs.localVideo.srcObject = stream;
                    })
                    .catch((err) => {
                        this.$lib.logError('获取navigator.mediaDevices.getUserMedia()失败,错误原因如下:');
                        this.$lib.logError(err);
                    });
            },
            // 发送消息给 Socket IO 服务器
            sendMessage(msg) {
                this.$socket.emit('message', msg)
            }
        },
        sockets: {
            connect() {
                this.$lib.logSuccess("连接socket io服务器成功");
            },
            message(msg) {
                this.$lib.logInfo("接收到服务端的消息:" + msg);
            }
        },
    }
</script>
  • 效果展示(如果 VCam播放视频,这里显示的是视频的内容)

 

2 - 效果展示

  1. 1端 create,1端 join:
  • 流程图片

 

image.png

  • 流程说明
    A为创建者,B为加入者(2个扮演的角色可调换)
    A用户创建房间(自定义房间名称:room_webrtc)
    同时Signal Server(Socket IO服务器)通知A用户,你是创建者
    B用户加入到room_webrtc
    同时Signal Server(Socket IO服务器)通知A用户有新的用户加入,通知B用户加入成功

  • 流程代码

data() return 中增加变量

 

// 房间名称
roomName: 'room_webrtc',
// 通道是否准备就绪
isChannelReady: false

methods:中增加方法:创建加入房间

 

// 试图创建或加入Socket IO的房间
createOrJoinRoom() {
    this.$lib.logInfo('试图创建或加入Socket IO 的房间:' + this.roomName);
    this.$socket.emit('create or join', this.roomName);
}

sockets:增加监听多个方法

通知用户(A)为创建者(created)
通知房间其他用户(A)有新伙伴(B)准备加入(join)
通知用户(B)加入成功(joined)
通知房间其他用户(A)新伙伴(B)加入成功(ready)
房间满了(full)

 

created(room) {
    this.$lib.logInfo('您是房间:' + room + ' 的创建者');

    // 赋值创建者标志
    this.isInitiator = true;
},
join(room) {
    this.$lib.logInfo('房间:' + room + ' 有新的用户准备加入');
},
joined(room) {
    this.$lib.logInfo('房间:' + room + ' 已经加入成功');
    
    // 加入者,设定通道准备就绪
    this.isChannelReady = true;
},
ready(){
    this.$lib.logInfo('房间:' + room + ' 新的用户已经加入成功');
    
    // 创建者,设定通道准备就绪
    this.isChannelReady = true;
},
full(){
    this.$lib.logInfo('房间:' + room + ' 已经满员了');
}
  1. create 端(创建者端,发送offer)
  • 流程图片

 

创建 RTCPeerConnection

  • 流程说明
    当远程伙伴加入成功后,创建端需开始创建 RTCPeerConnection(本地端机器与远端机器的一条对等连接)
    创建Offer 并 设置本地描述(SetLocalDescription)

  • 流程代码
    data() return 中增加变量

 

// RTCPeerConnection,本地端机器与远端机器的一条对等连接
pc: null,
// coturn配置供 RTCPeerConnection 使用,(如果没有coturn,可参照demo的step-05中的代码获取)
pcConfiguration: {
    iceServers: [{
        urls: 'turn:wzeros.cn',
        username: 'wzeros',
        credential: '123456'
    }]
},
// 远程媒体流
remoteStream: null,

methods:中增加方法:创建RTCPeerConnection

 

// 创建本地端机器与远端机器的一条对等连接
createPeerConnection() {
    try {
        this.$lib.logInfo('创建 RTCPeerConnection');
        this.pc = new RTCPeerConnection(this.pcConfiguration);
        this.pc.onicecandidate = this.handleIceCandidate;
        this.pc.onaddstream = this.handleRemoteStreamAdded;
        this.pc.onremovestream = this.handleRemoteStreamRemoved;

        this.$lib.logInfo('增加本地媒体流');
        this.pc.addStream(this.localStream);
    } catch (e) {
        this.$lib.logError('创建 RTCPeerConnection 出错,错误原因如下');
        this.$lib.logObj(e.message);
        return;
    }
},
// 事件触发器
// 只要本地代理ICE 需要通过信令服务器传递信息给其他对等端时就会触发
// 功能说明地址:https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/onicecandidate
handleIceCandidate(event) {
    this.$lib.logInfo('触发 icecandidate 事件,事件内容如下');
    this.$lib.logInfo(event);

    if (event.candidate) {
        this.sendMessage({
            type: 'candidate',
            label: event.candidate.sdpMLineIndex,
            id: event.candidate.sdpMid,
            candidate: event.candidate.candidate
        });
    } else {
        this.$lib.logInfo('End of candidates');
    }
},
// 处理远程流添加
handleRemoteStreamAdded(event) {
    this.$lib.logInfo('处理远程媒体流添加');
    
    // 赋值远程媒体流
    this.remoteStream = event.stream;
    // 展示远程媒体流
    this.$refs.remoteVideo.srcObject = this.remoteStream;
},
// 处理远程流撤掉
handleRemoteStreamRemoved(event) {
    this.$lib.logInfo('Remote stream removed. Event: ', event);
},
// 创建 offer
createOffer() {
    this.$lib.logInfo('发送offer给远程的伙伴');

    this.pc.createOffer((sessionDescription) => {
        this.$lib.logInfo('设置本地描述,描述内容如下:');
        this.$lib.logInfo(sessionDescription);
        this.pc.setLocalDescription(sessionDescription);

        // 通过Signal Server 发送(offer) sessionDescription 到远程伙伴
        this.sendMessage(sessionDescription);
    }, (event) => {
        this.$lib.logError('createOffer()出错,错误原因如下:');
        this.$lib.logError(event);
    });
}

sockets:ready中增加调用createPeerConnection

 

ready(room) {
    this.$lib.logInfo('房间:' + room + ' 新的用户已经加入成功');

    // 创建者 | 加入者,设定通道准备就绪
    this.isChannelReady = true;

    // 创建者
    if (this.isInitiator) {
        // 创建本地端机器与远端机器的一条对等连接
        this.createPeerConnection();
        // 创建 offer
        this.createOffer();
    }
}
  1. join端(加入者端,接收offer)
  • 流程图片

 

接收offer

  • 流程说明
    创建者端通过Signal Server(Socket IO服务器)发送出了offer
    加入者端在监听(message)消息后,需要初始化RTCPeerConnection等操作
    再发送一个回应信息(answer)给create端 并 设置本地描述(SetLocalDescription)

  • 流程代码

methods:中增加方法:创建回复

 

// 创建 answer
createAnswer() {
    this.$lib.logInfo('发送answer给远程的创建者');

    this.pc.createAnswer().then((sessionDescription) => {
        this.$lib.logInfo('设置会话描述,描述内容如下:');
        this.$lib.logObj(sessionDescription);
        this.pc.setLocalDescription(sessionDescription);
        
        // 通过Signal Server 发送(answer) sessionDescription 到远程的创建者
        this.sendMessage(sessionDescription);
    }, (error) => {
        this.$lib.logError('创建会话描述出错,错误原因如下:');
        this.$lib.logObj(error);
    });
}

sockets:修改message监听方法

 

message(msg) {
    this.$lib.logInfo('接收到服务端的消息,消息如下:');
    this.$lib.logObj(msg);
    if (msg.type) {
        switch (msg.type) {
            // 加入者接收到创建者的offer
            case 'offer':
                // 创建本地端机器与远端机器的一条对等连接
                this.createPeerConnection();
                // 设置远程回话描述
                this.pc.setRemoteDescription(new RTCSessionDescription(msg));
                // 给创建者回复
                this.createAnswer();
                break;
            default:
                break;
        }
    }
}
  1. create端(创建者端,接收answer)
  • 流程图片

 

接收answer

  • 流程说明
    加入者接收到offer后,返回answer,创建者端接收到answer后设定远程会话描述
    设置完会话描述后,此时在创建RTCPeerConnection使用了coturn服务配置的话,将会触发onicecandidate事件。如果建立连接成功后将会触发onaddstream(添加远程媒体流)
    在onicecandidate事件中做了一件事情发送type:'candidate'的内容到Signal Server服务器,继而推送到2端

  • 流程代码

sockets:修改message监听方法

 

message(msg) {
    this.$lib.logInfo('接收到服务端的消息,消息如下:');
    this.$lib.logObj(msg);
    if (msg.type) {
        switch (msg.type) {
            case 'offer': // 加入者接收到创建者的offer

                // 创建本地端机器与远端机器的一条对等连接
                this.createPeerConnection();
                // 设置远程会话描述
                this.pc.setRemoteDescription(new RTCSessionDescription(msg));
                // 给创建者回复
                this.createAnswer();
                break;
            case 'answer': // 创建者接收到加入者的answer

                // 设置远程会话描述
                this.pc.setRemoteDescription(new RTCSessionDescription(msg));
                break;
            default:
                break;
        }
    }
}
  1. 2端candidate处理
  • 流程图片

 

candidate处理

  • 流程说明
    双方只需要做的一件事情就是对RTCPeerConnection添加一个Ice代理后,之后自动触发添加远程媒体流,双方就能够进行通信了

  • 流程代码

sockets:修改message监听方法

 

message(msg) {
    this.$lib.logInfo('接收到服务端的消息,消息如下:');
    this.$lib.logObj(msg);
    if (msg.type) {
        switch (msg.type) {
            case 'offer': // 加入者接收到创建者的offer

                // 创建本地端机器与远端机器的一条对等连接
                this.createPeerConnection();
                // 设置远程会话描述
                this.pc.setRemoteDescription(new RTCSessionDescription(msg));
                // 给创建者回复
                this.createAnswer();
                break;
            case 'answer': // 创建者接收到加入者的answer

                // 设置远程会话描述
                this.pc.setRemoteDescription(new RTCSessionDescription(msg));
                break;
            case 'candidate': // candidate处理

                this.pc.addIceCandidate(new RTCIceCandidate({
                    sdpMLineIndex: msg.label,
                    candidate: msg.candidate
                }));
                break;
            default:
                break;
        }
    }
}
  1. 2端退出通信处理
  • 流程图片

 

退出通信处理

  • 流程说明
    只要有一方退出系统,发送离开消息到Signal Server服务器,信令服务器通知双方结束通信
    关闭RTCPeerConnection,暂停本地视频流和远程视频流展示

  • 流程代码
    methods:中增加方法:离开通信

 

leave() {
    if(this.remoteStream !== null) {
        this.$refs.remoteVideo.srcObject = null;
        this.remoteStream = null;
    }
    
    if(this.localStream !== null) {
        this.$refs.localVideo.srcObject = null;
        this.localStream = null;
    }
    
    if (this.pc !== null) {
        this.pc.close();
        this.pc = null;
    }
}

export default :中增加beforeDestroy监听

 

beforeDestroy() {
    this.sendMessage('leave');
}

sockets:修改message监听方法

 

message(msg) {
    this.$lib.logInfo('接收到服务端的消息,消息如下:');
    this.$lib.logObj(msg);
    if (msg.type) {
        switch (msg.type) {
            case 'offer': // 加入者接收到创建者的offer

                // 创建本地端机器与远端机器的一条对等连接
                this.createPeerConnection();
                // 设置远程会话描述
                this.pc.setRemoteDescription(new RTCSessionDescription(msg));
                // 给创建者回复
                this.createAnswer();
                break;
            case 'answer': // 创建者接收到加入者的answer

                // 设置远程会话描述
                this.pc.setRemoteDescription(new RTCSessionDescription(msg));
                break;
            case 'candidate': // candidate处理

                this.pc.addIceCandidate(new RTCIceCandidate({
                    sdpMLineIndex: msg.label,
                    candidate: msg.candidate
                }));
                break;
            case 'leave': // 离开通信

                this.leave();
                break;
            default:
                break;
        }
    }
}