WebRTC实践点对点通信

技术前言

前次教程我们一起用NodeJs做为服务端,用纯javascript简单实现了一个基于文本的P2P通讯,两个浏览器终端之间基于Socket.IO技术进行普通文本信息互相传输,其实已实现了一个简单聊天室的基本技术模型。

本次教程我们基于上次教程的内容继续探讨WebRTC视频通讯,实现不同浏览器之间进行视频通讯。

大家注意本系列教程是采用循序渐进的方式,每次教程的内容都是下次教程的铺垫。WebRTC视频通讯主要是用RTCPeerConnection通讯实现与其它终端的长连接完成双方的视频数据传输。

RTCPeerConnection连接过程图

WebRTC实践点对点通信

RTCPeerConnection视频流数据传输完整的生命周期还是比较清晰,如上图所示终端A和终端B在初始化的时候各自创建RTCPeerConnection对像准备数据通道。

终端A完成初始化后马上向终端B发送Call信令(标明媒体数据已准备就绪),终端B收到Call后发送Offer信令并设置setLocalDescription属性,注意当setLocalDescription属性改变时会触发onicecandidate事件,通常在onicecandidate事件中实现向对方发送candidate数据的业务逻辑。

此时终端A收到Offer后设置自身的setRemoteDescription属性,并且执行createAnswer操作设置setLocalDescription属性后立刻发送Answer信令。

终端B收到Answer后设置自身的setRemoteDescription属性同时触发onaddstream事件。在onaddstream事件中实现将远程视频流加载到本地视频容器中的操作。

至此终端A到终端B的P2P视频通讯完成。

接口方法

RTCPeerConnection 中提供的接口很多,大家可以去官网上查询。

我这里主要说的是有关本教程中的接口,也是最常用的接口。

1. createOffer

在 CreateOffer 中,会获取本地所支持的音视频编码格式,以及传输相关参数信息,一般在此方法中要设置setLocalDescription属性完成RTC对本地视频数据加载。

2. setLocalDescription

设置改变与本地连接关联的本地视频描述。此描述信息中包括媒体格式。如果setLocalDescription()在连接已经建立时被调用,则表示正在进行重新协商(可能是为了适应不断变化的网络状况)。

因为在两个对等方就配置达成一致之前将交换描述,所以通过调用提交的描述setLocalDescription()不会立即生效。相反,当前的连接配置将保持不变,直到协商完成。只有这样,商定的配置才会生效。

3. createAnswer

在WebRTC连接期间createAnswer()方法是针对收到的Offer信令后创建 SDP答复信令并回复远程终端。此SDP答复包含有关会话中已附加的所有媒体参数,如浏览器支持的编解码器和选项以及已收集的所有ICE候选者的信息。

4. setRemoteDescription

设置改变与远程连接关联的视频描述。该描述指定了连接远程终端的视频属性,包括媒体格式。通常在通过信令服务器从远程终端收到Answer答复后调用此方法。

因为在两个对等方就配置达成一致之前将交换描述,所以通过调用提交的描述setRemoteDescription()不会立即生效。相反,当前的连接配置将保持不变,直到协商完成。只有这样,商定的配置才会生效。

5. onaddstream

当执行addstream操作时,将发送此类事件。该事件在调用后立即发送setRemoteDescription(),并且不等待SDP协商的结果。

6. onicecandidate

每当本地ICE代理需要通过信令服务器将消息传递到远程终端时,就会触发事件。这使ICE代理可以与远程终端进行协商,而浏览器本身无需知道有关用于信令的技术的任何细节。只需实施此方法即可使用您选择的任何将ICE候选者发送到远程终端的消息传递技术。

实践代码

前端页面



	
		
		
		WebRTC实践点对点通信
		
		
		
		
		
		
		
	
	
		

WebRTC实践点对点通信

RTC代码

var rtc = (function() {
    ///通讯是否创建
    var isChannelReady = false;
    ///房间是否创建
    var isInitiator = false;
    var isStarted = false;
    var remoteStream;
    ///当前收到对方的房间号
    var curReceiveRoomId = "";
    var pc = new RTCPeerConnection(null);
    pc.onicecandidate = function(event) {
        trace('icecandidate event: ', event);
        if (event.candidate) {
            sendMessage({
                type: 'candidate',
                label: event.candidate.sdpMLineIndex,
                id: event.candidate.sdpMid,
                candidate: event.candidate.candidate
            });
        } else {
            trace('End of candidates.');
        }
    };
    pc.onaddstream = function(event) {
        trace('Remote stream added.');
        remoteStream = event.stream;
        document.getElementById("remoteVideo").srcObject = remoteStream;
    };
    $.net.join(function() {
            ///通道已准备就绪
            isChannelReady = true;
        })
        .call(doOffer)
        .offer(doAnswer)
        .answer(function(message) {
            if (isStarted) {
                pc.setRemoteDescription(new RTCSessionDescription(message));
            }
        })
        .candidate(function(message) {
            if (isStarted) {
                pc.addIceCandidate(new RTCIceCandidate({
                    sdpMLineIndex: message.label,
                    candidate: message.candidate
                }));
            }
        })
        .bye(function(message) {
            if (isStarted) {
                trace('对方连接已断开。');
                pc.close();
                pc = null;
                isStarted = false;
                isInitiator = false;
            }
        });
    function sendMessage(message) {
        $.net.meeting(message);
    }
    function doCall() {
        //var toid = $("input[name='g']:checked").val();
        var toid = rtc.curVideoFriendId;
        if (!toid) {
            alert('暂无目标好友。');
            return;
        }
        var cmd = {
            body: 'got user media',
            roomid: curReceiveRoomId ? curReceiveRoomId : $.net.id
        };
        $.net.send(cmd, toid, 'media');
        trace('发送' + JSON.stringify(cmd));
    }
    function doOffer(msg) {
        if (msg.roomid) {
            curReceiveRoomId = msg.roomid;
            trace('收到对方的房间编号 "' + curReceiveRoomId + '" !');
        }
        if (!isChannelReady) {
            trace('网络通讯没有建立不能发offer');
            return;
        }
        if (!isInitiator) {
            trace('不是房间创建者不能发offer');
            return;
        }
        trace('发送视频通话邀请函offer');
        pc.createOffer(function(description) {
            pc.setLocalDescription(description); //触发onicecandidate事件
            trace('setLocalAndSendMessage sending message', description);
            sendMessage(description);
        }, trace);
        isStarted = true;
    }
    function doAnswer(message) {
        trace('Sending answer to peer.');
        pc.setRemoteDescription(new RTCSessionDescription(message)); //触发onaddstream事件
        pc.createAnswer().then(
            function(description) {
                pc.setLocalDescription(description); //触发onicecandidate事件
                trace('setLocalAndSendMessage sending message', description);
                sendMessage(description);
            }, trace
        );
    }
    return {
        curVideoFriendId: '',
        close: function() {
            sendMessage('bye');
            /*
            var room = roomService.get();
            if(room.id == $.net.id) {
            	trace('房间 "' + room.id + '"已销毁。');
            	roomService.delete();
            }*/
            curReceiveRoomId = "";
        },
        room: function() {
            //var room = roomService.get();
            if (curReceiveRoomId) {
                $.net.meeting('join', curReceiveRoomId, function(r) {
                    trace('入进房间:', JSON.stringify(r));
                    isChannelReady = true;
                });
            } else {
                //room = roomService.create($.net.id);
                //room = roomService.create('testRoom');
                $.net.meeting('create', $.net.id, function(r) {
                    trace('创建房间', JSON.stringify(r));
                    isInitiator = true;
                });
            }
        },
        video: function(ctrl) {
            navigator.mediaDevices.getUserMedia({
                audio: false,
                video: true
            }).then(stream => {
                trace('本地视频已开启。');
                if (stream) {
                    pc.addStream(stream)
                    document.getElementById(ctrl).srcObject = stream;
                    ///本是视频就绪,向对方主播发送call通知
                    doCall();
                }
            });
        },
        call: function() {
            if (!rtc.curVideoFriendId) {
                alert('没有选择视频通话的目标好友!');
                return;
            }
            rtc.room();
            rtc.video("localVideo");
        },
        init: function() {
            window.onbeforeunload = rtc.close;
            $(document).delegate("#CloseBtn", "click", function() {
                rtc.close();
            });
        }
    }
})();

服务端(Nodejs)

var persons = [];
var getPersonSeq = function() {
    var tempSeq = Math.floor(Math.random() * 5) + 1;
    persons.forEach(p => {
        if (p.seq == tempSeq) {
            tempSeq = getPersonSeq();
        }
    });
    return tempSeq;
}
io.sockets.on('connection', function(socket) {
    if (persons.indexOf(socket.id) == -1) {
        persons.push({
            id: socket.id,
            seq: getPersonSeq()
        });
    }
    function trace() {
        console.log(arguments);
    }
    console.log("用户 '" + socket.id + "' 连接成功!");
    socket.emit('ready', socket.id, persons);
    socket.broadcast.emit('change', persons);
    socket.on('disconnect', function() {
        trace('终端(' + socket.id + ')已断开。 ');
        var tempPersons = [];
        persons.forEach(e => {
            if (e.id != socket.id) {
                tempPersons.push(e);
            }
        });
        persons = tempPersons;
        //rooms.del(socket.id);
        socket.broadcast.emit('change', persons);
    });
    socket.on('message', function(body) {
        var d = new Date();
        body.from = socket.id;
        body.time = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
        socket.to(body.to).emit('message', body);
        trace('终端(' + socket.id + "):message>", body);
    });
    socket.on('meeting', function(body) {
        ///向除自己外所有meeting终端广播消息
        socket.broadcast.emit('meeting', body);
        //sockets.in(room).emit('meeting', body);
        trace('终端(' + socket.id + "):meeting>", body);
    });
    socket.on('create', function(room, callbackfunc) {
        var clientsInRoom = io.sockets.adapter.rooms[room];
        var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
        var temproom = {
            id: room,
            count: numClients
        };
        socket.join(room);
        //rooms.save(temproom);
        if (typeof(callbackfunc) == 'function') {
            callbackfunc(temproom);
        }
    });
    socket.on('join', function(room, callbackfunc) {
        var clientsInRoom = io.sockets.adapter.rooms[room];
        var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
        var robj = {
            id: room,
            count: numClients
        }
        switch (numClients) {
            case 0:
                trace('终端(' + socket.id + ')进入的房间 ”' + room + '" 不存在!');
                socket.emit('empty', room);
                break;
            case 1:
                ///向房间room中发送消息
                io.sockets.in(room).emit('join', room);
                socket.join(room);
                trace('房间 ' + room + '中现在有' + numClients + '个终端!');
                break;
            case 2:
                socket.emit('full', room);
                trace('房间 ”' + room + '" 已满。');
                break;
        }
        if (typeof(callbackfunc) == 'function') {
            callbackfunc(robj);
        }
    });
    socket.on('ipaddr', function() {
        var ifaces = os.networkInterfaces();
        for (var dev in ifaces) {
            ifaces[dev].forEach(function(details) {
                if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
                    socket.emit('ipaddr', details.address);
                }
            });
        }
    });
});

运行结果

WebRTC实践点对点通信

实践历程

1. WebRTC实践简介

2. WebRTC实践获取视频流

3. WebRTC实践传输视频流

4. WebRTC实践信令服务

5. WebRTC实践点对点通信

6. WebRTC实践视频聊天室

7. WebRTC实践总结

展开阅读全文

页面更新:2024-04-11

标签:候选者   终端   对等   属性   接口   房间   浏览器   事件   通讯   通信   方法   教程   数据   技术   科技   信息   视频

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top