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的缘故,具体没有再去考察,如果后续有时间,我会去研究一下具体原因),如果哪位大佬了解原因,欢迎在评论区告知,在此非常感谢!!!