• 【开发记录】网页实时音视频通话系统视频会议系统WebRTC中实现局域网视频连接的步骤说明介绍


    去年TSINGSEE青犀视频研发团队基于WEBRTC架构开了网页音视频通话平台EasyRTC,EasyRTC支持微信小程序、H5页面、APP、PC客户端等接入方式之间互通,快速从零开始搭建实时音视频通信;支持多人至百万人视频通话,满足语音视频社交。

    微信截图_20201020104825.png

    如果大家对EasyRTC感兴趣,可以联系我们进行了解或试用。今年我们仍然没有停止对WEBRTC技术的探索。本文和大家分享一下通过webrtc实现局域网进行音视频连接的步骤。

    1、首先要创建服务代码,来确保服务器启动

    	const Koa = require('koa');
    const path = require('path');
    const koaSend = require('koa-send');
    const static = require('koa-static');
    const socket = require('koa-socket');
    const users = {}; // 保存用户
    const sockS = {}; // 保存客户端对应的socket
    const io = new socket({
        ioOptions: {
            pingTimeout: 10000,
            pingInterval: 5000,
        }
    });
    
    const https = require("https");
    const fs = require("fs");
    
    // 创建一个Koa对象表示web app本身:
    const app = new Koa();
    // socket注入应用
    io.attach(app);
    app.use(static(
        path.join( __dirname,  './public')
    ));
    
    // 对于任何请求,app将调用该异步函数处理请求:
    app.use(async (ctx, next) => {
        if (!/./.test(ctx.request.url)) {
            await koaSend(
                ctx,
                'index.html',
                {
                    root: path.join(__dirname, './'),
                    maxage: 1000 * 60 * 60 * 24 * 7,
                    gzip: true,
                } // eslint-disable-line
            );
        } else {
            await next();
        }
    });
    // io.on('join', ctx=>{ // event data socket.id
    // });
    app._io.on('connection', sock => {
        sock.on('join', data=>{
            console.log("join:", data);
            sock.join(data.roomid, () => {
                if (!users[data.roomid]) {
                    users[data.roomid] = [];
                }
                let obj = {
                    account: data.account,
                    id: sock.id
                };
                let arr = users[data.roomid].filter(v => v.account === data.account);
                if (!arr.length) {
                    users[data.roomid].push(obj);
                }
                sockS[data.account] = sock;
                app._io.in(data.roomid).emit('joined', users[data.roomid], data.account, sock.id); // 发给房间内所有人
                // sock.to(data.roomid).emit('joined',data.account);
            });
        });
        sock.on('offer', data=>{
            console.log('offer', data);
            sock.to(data.roomid).emit('offer',data);
        });
        sock.on('answer', data=>{
            console.log('answer', data);
            sock.to(data.roomid).emit('answer',data);
        });
        sock.on('__ice_candidate', data=>{
            console.log('__ice_candidate', data);
            sock.to(data.roomid).emit('__ice_candidate',data);
        });
    
        // 1 v 1
        sock.on('apply', data=>{ // 转发申请
            // console.log("apply:", data);
            sockS[data.account].emit('apply', data);
        });
        sock.on('reply', data=>{ // 转发回复
            console.log("reply:", data);
            sockS[data.account].emit('reply', data);
        });
        sock.on('1v1answer', data=>{ // 转发 answer
            sockS[data.account].emit('1v1answer', data);
        });
        sock.on('1v1ICE', data=>{ // 转发 ICE
            sockS[data.account].emit('1v1ICE', data);
        });
        sock.on('1v1offer', data=>{ // 转发 Offer
            sockS[data.account].emit('1v1offer', data);
        });
        sock.on('1v1hangup', data=>{ // 转发 hangup
            sockS[data.account].emit('1v1hangup', data);
        });
    });
    app._io.on('disconnect', (sock) => {
        for (let k in users) {
            users[k] = users[k].filter(v => v.id !== sock.id);
        }
        console.log(`disconnect id => ${users}`);
    });
    
    // 在端口3001监听:
    let port = 3001;
    app.listen(port, _ => {
        console.log('app started at port ...' + port);
    });
    
    

    2、在进行音视频通信是,必修要本地端口或者https服务。

    (1)网上下载OpenSSL软件
    (2)创建后缀名(.Key和.cert)证书;
    查看网址教程:https://www.cnblogs.com/tugenhua0707/p/10927722.html
    (3)在js服务把证书添加进去

    const options = {
        key: fs.readFileSync("./server.key", "utf8"),
        cert: fs.readFileSync("./server.cert", "utf8")
    };
    
    let server = https.createServer(options, app.callback()).listen(3002);
    app._io.listen(server);
    
    

    3、客户端代码采用VUE,如下(重要部分代码):

    <template>
        <div class="remote1"
             v-loading="loading"
             :element-loading-text="loadingText"
             element-loading-spinner="el-icon-loading"
             element-loading-background="rgba(0, 0, 0, 0.8)"
        >
            <div class="shade" v-if="!isJoin">
                <div class="input-container">
                    <input type="text" v-model="account" placeholder="请输入你的昵称" @keyup.enter="join">
                    <button @click="join">确定</button>
                </div>
            </div>
            <div class="userList">
                <h5>在线用户:{{userList.length}}</h5>
                <p v-for="v in userList" :key="v.account">
                    {{v.account}}
                    <i v-if="v.account === account || v.account === isCall">
                    {{v.account === account ? 'me' : ''}}
                    {{v.account === isCall ? 'calling' : ''}}
                    </i>
                    <span @click="apply(v.account)" v-if="v.account !== account && v.account !== isCall">呼叫 {{v.account}}</span>
                </p>
            </div>
            <div class="video-container" v-show="isToPeer">
                <div>
                    <video src="" id="rtcA" controls autoplay></video>
                    <h5>{{account}}</h5>
                    <button @click="hangup">hangup</button>
                </div>
                <div>
                    <video src="" id="rtcB" controls autoplay></video>
                    <h5>{{isCall}}</h5>
                </div>
            </div>
        </div>
    </template>
    <script>
        import socket from '../../utils/socket';
        export default{
            name: 'remote1',
            data() {
                return {
                    account: window.sessionStorage.account || '',
                    isJoin: false,
                    userList: [],
                    roomid: 'webrtc_1v1', // 指定房间ID
                    isCall: false, // 正在通话的对象
                    loading: false,
                    loadingText: '呼叫中',
                    isToPeer: false, // 是否建立了 P2P 连接
                    peer: null,
                    offerOption: {
                        offerToReceiveAudio: 1,
                        offerToReceiveVideo: 1
                    }
                };
            },
            methods: {
                join() {
                    if (!this.account) return;
                    this.isJoin = true;
                    window.sessionStorage.account = this.account;
                    socket.emit('join', {roomid: this.roomid, account: this.account});
                },
                initSocket() {
                    socket.on('joined', (data) => {
                        this.userList = data;
                    });
                    socket.on('reply', async data =>{ // 收到回复
                        this.loading = false;
                        console.log(data);
                        switch (data.type) {
                            case '1': // 同意
                                this.isCall = data.self;
                                // 对方同意之后创建自己的 peer
                                await this.createP2P(data);
                                // 并给对方发送 offer
                                this.createOffer(data);
                                break;
                            case '2': //拒绝
                                this.$message({
                                    message: '对方拒绝了你的请求!',
                                    type: 'warning'
                                });
                                break;
                            case '3': // 正在通话中
                                this.$message({
                                    message: '对方正在通话中!',
                                    type: 'warning'
                                });
                                break;
                        }
                    });
                    socket.on('apply', (data) => { // 收到请求
                        if (this.isCall) {
                            this.reply(data.self, '3');
                            return;
                        }
    
                        this.$confirm(data.self + ' 向你请求视频通话, 是否同意?', '提示', {
                            confirmButtonText: '同意',
                            cancelButtonText: '拒绝',
                            type: 'warning'
                        }).then(async () => {
                            await this.createP2P(data); // 同意之后创建自己的 peer 等待对方的 offer
                            this.isCall = data.self;
                            this.reply(data.self, '1');
                        }).catch(() => {
                            this.reply(data.self, '2');
                        });
                    });
                    socket.on('1v1answer', (data) =>{ // 接收到 answer
                        this.onAnswer(data);
                    });
                    socket.on('1v1ICE', (data) =>{ // 接收到 ICE
                        this.onIce(data);
                    });
                    socket.on('1v1offer', (data) =>{ // 接收到 offer
                        this.onOffer(data);
                    });
                    socket.on('1v1hangup', _ =>{ // 通话挂断
                        this.$message({
                            message: '对方已断开连接!',
                            type: 'warning'
                        });
                        this.peer.close();
                        this.peer = null;
                        this.isToPeer = false;
                        this.isCall = false;
                    });
                },
                hangup() { // 挂断通话
                    socket.emit('1v1hangup', {account: this.isCall, self: this.account});
                    this.peer.close();
                    this.peer = null;
                    this.isToPeer = false;
                    this.isCall = false;
                },
                apply(account) {
                    // account 对方account  self 是自己的account
                    this.loading = true;
                    this.loadingText = '呼叫中';
                    socket.emit('apply', {account: account, self: this.account});
                },
                reply(account, type) {
                    socket.emit('reply', {account: account, self: this.account, type: type});
                },
                async createP2P(data) {
                    this.loading = true;
                    this.loadingText = '正在建立通话连接';
                    await this.createMedia(data);
                },
                async createMedia(data) {
                    // 保存本地流到全局
                    try {
                        this.localstream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
                        let video = document.querySelector('#rtcA');
                        video.srcObject = this.localstream;
                        console.log("发送端:", Date.now());
                    } catch (e) {
                        console.log('getUserMedia: ', e)
                    }
                    this.initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
                },
                initPeer(data) {
                    // 创建输出端 PeerConnection
                    let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
                    this.peer = new PeerConnection();
                    this.peer.addStream(this.localstream); // 添加本地流
                    // 监听ICE候选信息 如果收集到,就发送给对方
                    this.peer.onicecandidate = (event) => {
                        if (event.candidate) {
                            socket.emit('1v1ICE', {account: data.self, self: this.account, sdp: event.candidate});
                        }
                    };
                    this.peer.onaddstream = (event) => { // 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src
                        this.isToPeer = true;
                        this.loading = false;
                        let video = document.querySelector('#rtcB');
                        video.srcObject = event.stream;
                        console.log("接送端:", Date.now());
                    };
                },
                async createOffer(data) { // 创建并发送 offer
                    try {
                        // 创建offer
                        let offer = await this.peer.createOffer(this.offerOption);
                        // 呼叫端设置本地 offer 描述
                        await this.peer.setLocalDescription(offer);
                        // 给对方发送 offer
                        socket.emit('1v1offer', {account: data.self, self: this.account, sdp: offer});
                    } catch (e) {
                        console.log('createOffer: ', e);
                    }
                },
                async onOffer(data) { // 接收offer并发送 answer
                    try {
                        // 接收端设置远程 offer 描述
                        await this.peer.setRemoteDescription(data.sdp);
                        // 接收端创建 answer
                        let answer = await this.peer.createAnswer();
                        // 接收端设置本地 answer 描述
                        await this.peer.setLocalDescription(answer);
                        // 给对方发送 answer
                        socket.emit('1v1answer', {account: data.self, self: this.account, sdp: answer});
                    } catch (e) {
                        console.log('onOffer: ', e);
                    }
                },
                async onAnswer(data) { // 接收answer
                    try {
                        await this.peer.setRemoteDescription(data.sdp); // 呼叫端设置远程 answer 描述
                    } catch (e) {
                        console.log('onAnswer: ', e);
                    }
                },
                async onIce(data) { // 接收 ICE 候选
                    try {
                        await this.peer.addIceCandidate(data.sdp); // 设置远程 ICE
                    } catch (e) {
                        console.log('onAnswer: ', e);
                    }
                }
            },
            mounted() {
                this.initSocket();
                if (this.account) {
                    this.join();
                }
            }
        }
    </script>
    <style lang="scss" scoped>
        .remote1{
             100%;
            height: 100%;
            display: flex;
            justify-content: flex-start;
        }
        .shade{
            position: fixed;
            100vw;
            height: 100vh;
            left: 0;
            top:0;
            z-index: 100;
            background-color: rgba(0,0,0,0.9);
            .input-container{
                position: absolute;
                left:50%;
                top:30%;
                transform: translate(-50%, 50%);
                display: flex;
                justify-content: space-between;
                align-items: center;
                input{
                    margin: 0;
                }
            }
        }
        .userList{
            border: 1px solid #ddd;
            margin-right: 50px;
            h5{
                text-align: left;
                margin-bottom: 5px;
            }
            p{
                border-bottom: 1px solid #ddd;
                line-height: 32px;
                200px;
                position: relative;
                overflow: hidden;
                cursor: pointer;
                span{
                    position: absolute;
                    left:0;
                    top:100%;
                    background-color: #1fbeca;
                    color: #fff;
                    height: 100%;
                    transition: top 0.2s;
                    display: block;
                     100%;
                }
                i{
                    font-style: normal;
                    font-size: 11px;
                    border: 1px solid #1fbeca;
                    color: #27cac7;
                    border-radius: 2px;
                    line-height: 1;
                    display: block;
                    position: absolute;
                    padding: 1px 2px;
                    right: 5px;
                    top: 5px;
                }
            }
            p:last-child{
              border-bottom: none;
            }
            p:hover span{
                top:0;
            }
        }
        .video-container{
            // display: flex;
            // justify-content: center;
            video{
                 400px;
                height: 300px;
                margin-left: 20px;
                background-color: #ddd;
            }
        }
    </style>
    
    

    说明

    首先必修启动服务端建立连接,在通过呼叫对方id进行识别;建立PeerConnection视频连接,并把SDP的ICE候选者传递candidate给远程对等体。

  • 相关阅读:
    论文连接
    MySQL中的datetime与timestamp比较
    查看挂载情况
    insertable = false, updatable = false的使用
    umount: /home: device is busy
    LVM
    erase-credentials配置
    <T> List<T>前面<T>的意思
    Java 内部类 this
    AuthenticationManager, ProviderManager 和 AuthenticationProvider
  • 原文地址:https://www.cnblogs.com/EasyNVR/p/13897978.html
Copyright © 2020-2023  润新知