WebRTC实践点对点通信

技术前言

       前次教程我们一起用NodeJs做为服务端,用纯javascript简单实现了一个基于文本的P2P通讯,两个浏览器终端之间基于Socket.IO技术进行普通文本信息互相传输,其实已实现了一个简单聊天室的基本技术模型。本次教程我们基于上次教程的内容继续探讨WebRTC视频通讯,实现不同浏览器之间进行视频通讯。大家注意本系列教程是采用循序渐进的方式,每次教程的内容都是下次教程的铺垫。WebRTC视频通讯主要是用RTCPeerConnection通讯实现与其它终端的长连接完成双方的视频数据传输。

RTCPeerConnection连接过程图
在这里插入图片描述
      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候选者发送到远程终端的消息传递技术。

实践代码

前端页面

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
		<title>WebRTC实践点对点通信</title>
		<script src="../javascript/trace.js"></script>
		<script src="../javascript/socket.io.js"></script>
		<script src="../javascript/jquery-1.11.1.min.js"></script>
		<script src="../javascript/jquerysocketio.js"></script>
		<script src="../javascript/rtc.js"></script>
		<style type="text/css">
			video {
				border: 1px solid black;
				background-color: black;
				max-width: 100%;
				width: 100px;
			}
		</style>
		<script>
			var cfg = {
				ip: '127.0.0.1',
				port: 6100
			}
			$(function() {
				$.net.connect(function(person) {
					trace("在线人数:" + person);
					if(person.length > 0) {
						$("#personDiv").empty();
						person.forEach(p => {
							if(p.id != $.net.id) {
								var html = '<input type="radio" value="' + p.id + '" name="g" id="' + p.id + '" /><label for="' + p.id + '">' + p.id + '</label>';
								$("#personDiv").append(html);
							}
						});
					}
				});
				$.net.receive(function(body) {
					trace("收到:" + JSON.stringify(body));
				});
				rtc.init();
				$("#StartBtn").click(function() {
					rtc.call();
				});
				$("#personDiv").delegate("input[type='radio']", "click", function() {
					rtc.curVideoFriendId=this.value;
				});
			})
		</script>
	</head>

	<body>
		<h1>WebRTC实践点对点通信</h1>
		<video id="localVideo" autoplay playsinline></video>
		<video id="remoteVideo" autoplay playsinline></video>
		<input type="button" value="Start" id="StartBtn" />
		<input type="button" value="Close" id="CloseBtn" />
		<div id="personDiv"></div>
	</body>

</html>

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);
                }
            });
        }
    });

});

运行结果

在这里插入图片描述

实践历程

1. WebRTC实践简介
2. WebRTC实践获取视频流
3. WebRTC实践传输视频流
4. WebRTC实践信令服务
5. WebRTC实践点对点通信
6. WebRTC实践视频聊天室
7. WebRTC实践总结

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

黒木涯

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

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

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

打赏作者

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

抵扣说明:

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

余额充值