webrtc 信令理解

p2p信令交互过程

借助WebRTC源码快速构建自己的音视频对聊功能。都是需要通过信令进行交互一些相关信息,需要借助信令服务器做一个消息中转,所以客户端A和客户端B进行发送音视频之前,客户端A先要去连接信令服务器,客户端B也需要先连接服务器。当客户端B连接服务器之后,就会通知客户端A对端已经上线。此时客户端A就会创建对端连接对象,发送握手请求,请求turn服务器获取自己的ip地址和发送canditate给客户B。客户端B也需要类似的流程。

P2P的交互时序图如下所示:

环境安装

nodejs 安装

此过程网上教程比较多,在此不再过多描述。

注意:安装io模块的是,最好放到nodejs 安装目录下

turn / stun服务器的安装和测试

不在本篇文章中描述,后续可能会有单独的篇章进行操作

代码编写

当前代码只是一些我认为重要的代码,并不是一个完整的信令服务器代码。我目前的要求是,只要我的webrtc能在两个页面上能正确执行就可以了,因此只能是一个参考。

信令服务器代码,因为在网上进行查询,看到大部分的资料都是使用nodejs,因此本次代码也是使用nodejs进行编写。此代码肯定不是良好的代码,但是对于我自己的目的是已经达到。所以记录下来,希望对webrtc新手有所帮助(我自身也是刚入坑的新人)。代码连接地址:127.0.0.1:8081

var os = require('os');
const static = require('node-static');
const http = require('http');
const { Socket } = require('dgram');
const file = new static.Server();

var clientsInRoom = {}

const app = http.createServer(function(req, res) {
    file.serve(req, res);
}).listen(8081);

const io = require('socket.io')(app);


io.on('connection', (socket) => {
    function log() {
        const array = ['>>> Message form server: '];
        for (var i = 0; i < arguments.length; ++i) {
            array.push(arguments[i]);
        }
        socket.emit('log', array);
    }

    socket.on('message', function (message) {
        log('client said', message);

        socket.broadcast.emit('message', message);
    });

    socket.on('create or join', function(room) {
        log('Received request to create or join room' + room);
        
        curRoom = clientsInRoom[room] = clientsInRoom[room] || [];
        log('room ' + room + " number is " + curRoom.length);
        if (curRoom.length === 0) {
            socket.join(room);
            log('Client ID ' + socket.id + ' create room ' + room);
            curRoom.push(socket);
            socket.emit('created', room, socket.id);
        } else {
            var current_room = curRoom;
            var num = current_room.length;
            log('room num' + num);
            if (num == 1) {
                log('Client ID ' + socket.id + ' create room ' + room);
                io.sockets.in(room).emit('join', room, socket.id);
                socket.join(room);
                
                var peer_id = "";
                for (var i = 0; i < current_room.length; ++i) {
                    log('server socket:' + current_room[i].id);
                    if (current_room[i].id !== socket.id) {
                        log('notify peer_join' + current_room[i].id);
                        current_room[i].emit('peer_join', room, socket.id);
                        peer_id = current_room[i].id;
                    }
                }

                current_room.push(socket);
                socket.emit('joined', room, socket.id, peer_id);
                
               // io.sockets.in(room).emit('ready');
                
            }
            else {
                socket.emit('full', room);
            }
        }
        
    });

    socket.on('ice_candidata', function(data, room) {
        var current_room = clientsInRoom[room] = clientsInRoom[room] || [];
        if (current_room.length !== 2) {
            log('room: ' + room + ' is empty');
            return;
        }

        var other_socket = current_room[0];
        if (socket.id === other_socket.id) {
            other_socket = current_room[1];
        }
        
        other_socket.emit('ice_candidata', data, socket.id);
    });

    socket.on('ipaddr',function(){
        var iface = os.networkInterfaces();
        for (var dev in iface) {
            iface[dev].forEach(function(details) {
                if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
                    socket.emit('ipaddr', details.address);
                }
            });
        }
    }) ;

    socket.on('offer', function(sdp, room) {
        log('offer')
        var current_room = clientsInRoom[room] = clientsInRoom[room] || [];
        if (current_room.length !== 2) {
            log('room: ' + room + ' is empty');
            return;
        }

        for (var i = 0; i < current_room.length; ++i) {
            var other_socket = current_room[i];
            if (other_socket.id !== socket.id) {
                other_socket.emit('offer', sdp);
            }
        }
    });

    socket.on('answer', function(sdp, room) {
        log('answer');
        var current_room = clientsInRoom[room] = clientsInRoom[room] || [];
        if (current_room.length !== 2) {
            log('room: ' + room + ' is empty');
            return;
        }

        for (var i = 0; i < current_room.length; ++i) {
            var other_socket = current_room[i];
            if (other_socket.id !== socket.id) {
                other_socket.emit('answer', sdp);
            }
        }
    });
});

客户端的代码分成两个部分,一个部分是html代码,一个部分是main.js代码,在main.js代码中,配置ice server 地址,我会在代码中注释说明。

首先是html代码:

<html> 
    <head>
        <title>webrtc client -- 20210527</title>
    </head>

    <body>
        <div style="float: left; width: 480;">
            <text>webrtc client -- 20210527</text>
            <div id="room_div">
                <input id="room_input" type="text" />
                <button onclick="add_room()"> join room</button>
            </div>

            <div id="rtc_content">
                <div>
                    <input id="msg_input" type="text" />
                    <button onclick="send()"> send message</button>
                </div>

                <div>chat info</div>
                <div>
                    <textarea id="msg_content" style="width: 450; height: 280px;"></textarea>
                </div>
            </div>
        </div>
        <div id="videos" style="float: right;">
            <video id="self" style="width: 320; height: 240;" autoplay> </video>
            <video id="remoteVideo" style="width: 320; height: 240;" muted="muted" autoplay playsinline></video>
        </div>
        
       
        <script type="text/javascript" src='/socket.io/socket.io.js'></script>
        <script type="text/javascript" src='js/client.js'></script>
        <script>
            var rtcSdk = RtcSdk();
            var videos= document.getElementById('videos');
            function add_room() {
                var room = document.getElementById("room_input").value;
                console.log('room is ' + room);
                rtcSdk.connect('ws://127.0.0.0:8081', room);
            }

            function send() {
                var msg = document.getElementById('msg_input');
                
                var chat_info = document.getElementById('msg_content');
                chat_info.value += "you: " + msg.value +"\r\n";

                rtcSdk.sendMessage(msg.value);
                msg.value = "";
            }

            rtcSdk.on('message', function(message){
                var chat_info = document.getElementById('msg_content');
                chat_info.value += "other: " + message +"\r\n";
            });

            rtcSdk.on('connected', function(room, socket) {
                console.log('join room:' + room + " socket: "+ socket);
                rtcSdk.createStream({"video":true});
            });

            rtcSdk.on('stream_created', function(stream) {
                try {
                    document.getElementById('self').srcObject = stream;
                } catch (error) {
                    document.getElementById('self').src = window.URL.createObjectURL(stream);
                }
                document.getElementById('self').play();
            });
            rtcSdk.on("stream_create_error", function(err) {
                alert("create stream failed!");
            });

            rtcSdk.on('pc_add_stream', function(stream, pc) {
                rtcSdk.attachStream(stream, "remoteVideo");
            });

        </script>

       
    </body>
</html>

接下来是main.j代码

var RtcSdk = function() {
    var PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection);
    var getUserMedia = (navigator.getUserMedia || navigator.webkitUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMeida)
    var nativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate);
    var nativeRTCSessionDescription = (window.mozRTCSessionDescription || window.RTCSessionDescription);
    var iceServer = {
        "iceServers": [{
            "url": "stun:xxx.xxx.xxx.xxx:3478", // ip 地址,可以配置在阿里云或者其他的地方
            "username": "xxx", // stun 服务器上配置的用户名
            "password": "xxx", // stun 服务器上配置的密码
        }]
        
    };

    function EventEmitter() {
        this.events = {};
    };

    EventEmitter.prototype.on = function(eventName, callback) {
        this.events[eventName] = this.events[eventName] || [];
        this.events[eventName].push(callback);
    };

    EventEmitter.prototype.emit = function(eventName, _){
        var events = this.events[eventName];
        if (!events) {
            return;
        }

        var args = Array.prototype.slice.call(arguments, 1);
        var i, m;
        for (i = 0, m = events.length; i < m; i++) {
            events[i].apply(null, args);
        }
    };

    function rtc() {
        this.localMediaStream = null;
        this.room = null;
        this.socket = null;
        this.isInitiator = false;
        this.numStreams = 0;
        this.initializedStreams = 0;
        this.peerSocketId = "";
        this.peerConnection = null;
    }

    rtc.prototype = new EventEmitter();

    rtc.prototype.connect = function(server, room) {
        if (room === "") {
            console.log('room is empty');
            return;
        }

        this.room = room;
        var socket;
        var self = this;
        socket = this.socket = io.connect();
        console.log('joining room:' + room);
        socket.emit('create or join', room);

        socket.on('created', function(room, clientId) {
            self.isInitiator = true;
            self.emit('connected', room, clientId);
        });
        socket.on('joined', function(room, self_id, peer_id) {
            self.isInitiator = false;
            self.peerSocketId = peer_id;
            self.emit('connected', room, self_id);
        });
        socket.on('join', function(room, peerId){
            console.log('clinet ' + peerId + ' joining room:' + room);
        });
        socket.on('peer_join', function(room, peerId) {
            console.log('peer_join, peer Id:' + peerId + ' joind room:' + room);
            self.peerSocketId = peerId;
            var pc = self.createPeerConnection();
           // pc.addStream(self.localMediaStream);
        });
        socket.on('full', function(room){
            alert(room + 'is full');
        });
        socket.on('log', (array) => {
            console.log.apply(console, array);
        });

        socket.on('message', function(message) {
            self.emit('message', message);
        });

        socket.on('ice_candidata', function(data, socket_id) {
            var candidate = new nativeRTCIceCandidate({
                sdpMLineIndex: data.label,
                candidate: data.candidate
            });
            var pc = self.peerConnection;
            pc.addIceCandidate(candidate);
        });

        socket.on('offer', function(sdp) {
            self.emit('answer_offer', sdp);
        });

        socket.on('answer', function(sdp) {
            self.reciveAnswer(sdp);
        });

        this.on('ready', function() {
            console.log('ready to perr');
            self.createPeerConnection();
            self.addStreams();
            self.sendOffer();
        });

        this.on('answer_offer', function(sdp) {
            console.log('answer offer');
            self.answerOffer(sdp);
        });
    };

    

    rtc.prototype.iceCandidate = function(data, socket_id) {
        var candidate = nativeRTCIceCandidate({
            sdpMLineIndex: data.label,
            candidate: data.candidate
        });
        var pc = self.peerConnection;
        pc.addIceCandidate(candidate);
    };

    rtc.prototype.sendMessage = function(message) {
        var self = this;
        self.socket.emit('message', message);
    };

    rtc.prototype.createStream = function(options) {
        var self  = this;

        options.video = !!options.video;
        options.audio = !!options.audio;

        if (getUserMedia) {
            this.numStreams++;

            getUserMedia.call(navigator, options, function(stream) {
                self.localMediaStream = stream;
                self.initializedStreams++;
                self.emit("stream_created", stream);
                if (self.initializedStreams == self.numStreams) {
                    self.emit("ready");
                }
            },
            function(error) {
                self.emit("stream_create_error", error);
            });
        } else {
            self.emit("stream_create_error", new Error('WebRTC is not yet supported in this browser.'));
        }
    };

    rtc.prototype.createPeerConnection = function() {
        var self = this;
        if (this.peerConnection !== null) {
            return this.peerConnection;
        }
        console.log("create peer connection");
        var pc = new PeerConnection(iceServer);
        this.peerConnection = pc;
        pc.onicecandidate = function(evt) {
            if (evt.candidate) {
                console.log("send ice candidata");
                self.socket.emit('ice_candidata',
                {
                    "label": evt.candidate.sdpMLineIndex,
                    "candidate": evt.candidate.candidate
                },
                self.room);
            }
        };
        pc.onopen = function(evt) {
            console.log('pc opened');
        };
        pc.onaddstream = function(evt) {
            self.emit('pc_add_stream', evt.stream, pc);
        }
        return pc;
    };

    rtc.prototype.addStreams = function () {
        if (null == this.localMediaStream) {
            return;
        }
        this.peerConnection.addStream(this.localMediaStream);
    };

    rtc.prototype.attachStream = function(stream, domId) {
        try {
            document.getElementById(domId).srcObject = stream;
        } catch (error) {
            document.getElementById(domId).src = window.URL.createObjectURL(stream);
        }
        document.getElementById(domId).play();
    };

    rtc.prototype.sendOffer = function() {
        var self = this;
        if (this.peerSocketId === '') {
            return;
        }

        var pcCreateOfferCbGen = function(pc, room) {
            return function(session_desc) {
                pc.setLocalDescription(session_desc);
                self.socket.emit('offer', session_desc, room);
            };
        };

        var pcCreateOfferErrorCb = function(err) {
            console.log(err);
        };
        var pc = this.peerConnection;
        console.log("create offer");
        pc.createOffer(pcCreateOfferCbGen(pc, this.room), pcCreateOfferErrorCb);
    };

    rtc.prototype.answerOffer = function(sdp) {
        var self = this;
        var pc = this.peerConnection;
        pc.setRemoteDescription(new nativeRTCSessionDescription(sdp));
        console.log("crate answer");
        pc.createAnswer(function(session_desc) {
            console.log("set local description");
            pc.setLocalDescription(session_desc);
            console.log("send answer");
            self.socket.emit('answer', session_desc, self.room);
        }, function(err) {
            console.log("err:" + err);
        });
    };

    rtc.prototype.reciveAnswer = function(sdp) {
        console.log('reciveAnswer');
        var pc = this.peerConnection;
        pc.setRemoteDescription(new nativeRTCSessionDescription(sdp));
    }

    return new rtc();
};

最后放置一个效果图:

demo下载地址:https://download.csdn.net/download/jiejieaiai/20082808(如果有更改,后续发布评论区)

存在问题

目前只能是127.0.0.0 地址访问,其他地址会提示打不开设摄像头(是不是因为不是https的缘故,具体没有再去考察,如果后续有时间,我会去研究一下具体原因),如果哪位大佬了解原因,欢迎在评论区告知,在此非常感谢!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值