WebRTC学习(三)
SDP规范
- 会话层(全局的,类似于全局变量)
- 媒体层(局部的,类似于局部变量)
会话层
- 会话的名称与目的
- 会话的存活时间
- 会话中包括多个媒体信息
SDP媒体信息
- 媒体格式
- 传输协议
- 传输IP和端口(价值不大,因为webrtc中会使用ice中收集的IP和端口)
- 媒体负载类型
SDP格式
- 由多个
<type>=<value>
组成 - 一个会话级描述
- 多个媒体级描述
SDP结构
- Session Description
- Time Description
- Media Description
Session Description
- v=(protocol version)
- 0=(owner/create and session identifiter)
- s=(session name)
- c=*(conn info - optional if included at session-level)
- a=*(zero or more session attribute lines)
Time Description
- t=(time the session is active)
- r=*(zero or more repeat times)
Media Description
- m=(media name and transport address)
- c=*(conn info - optional if included at session-level)
- b=*(bandwidth information)
- a=*(zero or more session attribute lines)
字段含义
- Version必选
v=0 SDP的版本号,不包括次版本号
- Session Name 必选
s=
<session name>
会话名,s=- 表示忽略会话名
- Origion/Owner 必选
o=
<username><session id ><version><network type><address type><address>
例子:0=- 701812417903490901324124 2 IN IP4 127.0.0.1
- Connection Data可选
c=
<network type><address type><connection address>
例子: c=IN IP4 0.0.0.0
- Media Announcements 必选
m=
<media><port><transport><fmt/payload type list>
例子: m= audio 1024 UDP/TLS/RTP/SAVF 111103 104 9 0 8 106 105 13 126
- Suggested Attributes 可选
a=
<TYPE>或a=<TYPE>:<VALUES>
例子: a=framerate:《帧速率》
- rtpmap 可选
a=rtpmap:
<fmt/payload type><encoding name>/<clockrate>[/</encodingparameters>]
例子: a=rtpmap:103 ISAC/16000
- fmtp可选
1.pnga=fmtp:
<format/payload type> parameters
例子:a=fmtp:103 apt=106
STUN/TURN服务器选型
- rfc5766-turn-server:谷歌开源,但是比较老了
- coTurn :rfc的升级版,比较活跃,选这个
- ResTurn :也是比较流行的,但是比较老
coTurn服务器搭建与部署
- 下载coTurn
- ./configure --prefix=/usr/local/turn
- 编译 make&&make install
一、下载corturn
//将安装的文件克隆到本地
git clone https://github.com/coturn/coturn
//安装依赖libevent-dev
sudo apt-get install libevent-dev
//进入corturn执行下面的代码。注意此处后面的路径是安装的路径,可以自选;执行成功如下
./configure --prefix=/usr/local/coturn
2.png
执行成功之后查看makefile
ls -alt Makefile
3.png
二、使用make工具,可以使用多线程进行并行的编译(一般后面是内核的两倍,例如:电脑内核为6,这里-j后面为12)
make -j 12
4.png
三、最后install
5.pngsudo make install
安装完毕,在/usr/local/corturn下面就可以看到安装的corturn了 /bin下面就是可执行的服务 /etc下面就是一些配置 /include下面是一些头文件 /lib下面是一些库文件
coTurn服务器配置
6.png修改turnserver.conf.default文件
smileyqp@smileyqp:/usr/local/coturn/etc$ sudo vim turnserver.conf.default
//简单设置几个参数
turnserver.conf.default
user=smileyqp:123456
listening-port=3478
环境变量中添加coturn
sudo vim ~/.bashrc
export PATH=/usr/local/coturn/bin
source ~/.bashrc
开启服务
//如果没添加到环境变量种种就用,加了就直接turnserver
./bin/turnserver -c ./etc/turnserver.conf.default
//查看turn服务是否启动
ps -ef | grep turn
PTCPeerConnection
7.png 8.png 9.png 10.png 11.png 12.png 13.png客户端信令消息
-
message中分为三个:offer、answer、candidate
14.png
音视频实时互动信令服务器
'use strict'
var log4js = require('log4js');
var http = require('http');
var https = require('https');
var fs = require('fs');
var socketIo = require('socket.io');
var express = require('express');
var serveIndex = require('serve-index');
var USERCOUNT = 3;
log4js.configure({
appenders: {
file: {
type: 'file',
filename: 'app.log',
layout: {
type: 'pattern',
pattern: '%r %p - %m',
}
}
},
categories: {
default: {
appenders: ['file'],
level: 'debug'
}
}
});
var logger = log4js.getLogger();
var app = express();
app.use(serveIndex('./public'));
app.use(express.static('./public'));
//http server
var http_server = http.createServer(app);
http_server.listen(80, '0.0.0.0');
var options = {
key : fs.readFileSync('./cert/1557605_www.learningrtc.cn.key'),
cert: fs.readFileSync('./cert/1557605_www.learningrtc.cn.pem')
}
//https server
var https_server = https.createServer(options, app);
var io = socketIo.listen(https_server);
io.sockets.on('connection', (socket)=> {
socket.on('message', (room, data)=>{
socket.to(room).emit('message',room, data);
});
socket.on('join', (room)=>{
socket.join(room);
var myRoom = io.sockets.adapter.rooms[room];
var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
logger.debug('the user number of room is: ' + users);
if(users < USERCOUNT){
socket.emit('joined', room, socket.id); //发给除自己之外的房间内的所有人
if(users > 1){
socket.to(room).emit('otherjoin', room, socket.id);
}
}else{
socket.leave(room);
socket.emit('full', room, socket.id);
}
//socket.emit('joined', room, socket.id); //发给自己
//socket.broadcast.emit('joined', room, socket.id); //发给除自己之外的这个节点上的所有人
//io.in(room).emit('joined', room, socket.id); //发给房间内的所有人
});
socket.on('leave', (room)=>{
var myRoom = io.sockets.adapter.rooms[room];
var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
logger.debug('the user number of room is: ' + (users-1));
//socket.emit('leaved', room, socket.id);
//socket.broadcast.emit('leaved', room, socket.id);
socket.to(room).emit('bye', room, socket.id);
socket.emit('leaved', room, socket.id);
//io.in(room).emit('leaved', room, socket.id);
});
});
https_server.listen(443, '0.0.0.0');
客户状态机及处理逻辑
18.png 19.png 20.png 21.png 22.png 23.png//main.js
'use strict'
var start = document.querySelector('button#start');
var restart = document.querySelector('button#restart');
var pc1 = new RTCPeerConnection();
var pc2 = new RTCPeerConnection();
function handleError(err){
console.log('Failed to create offer', err);
}
function getPc1Answer(desc){
console.log('getPc1Answer', desc.sdp);
pc2.setLocalDescription(desc);
pc1.setRemoteDescription(desc);
/*
pc2.createOffer({offerToRecieveAudio:1, offerToReceiveVideo:1})
.then(getPc2Offer)
.catch(handleError);
*/
}
function getPc1Offer(desc){
console.log('getPc1Offer', desc.sdp);
pc1.setLocalDescription(desc);
pc2.setRemoteDescription(desc);
pc2.createAnswer().then(getPc1Answer).catch(handleError);
}
function getPc2Answer(desc){
console.log('getPc2Answer');
pc1.setLocalDescription(desc);
pc2.setRemoteDescription(desc);
}
function getPc2Offer(desc){
console.log('getPc2Offer');
pc2.setLocalDescription(desc);
pc1.setRemoteDescription(desc);
pc1.createAnswer().then(getPc2Answer).catch(handleError);
}
function startTest(){
pc1.createOffer({offerToReceiveAudio:1, offerToRecieveVideo:1})
.then(getPc1Offer)
.catch(handleError);
}
function getMediaStream(stream){
stream.getTracks().forEach((track) => {
pc1.addTrack(track, stream);
});
var offerConstraints = {
offerToReceiveAudio: 1,
offerToRecieveVideo: 1,
iceRestart:false
}
pc1.createOffer(offerConstraints)
.then(getPc1Offer)
.catch(handleError);
}
function startICE(){
var constraints = {
audio: true,
video: true
}
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
}
start.onclick = startTest;
restart.onclick = startICE;
<html>
<head>
<title>
test createOffer from different client
</title>
</head>
<body>
<button id="start">Start</button>
<button id="restart">reStart ICE</button>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
实战demo
//main.js
'use strict'
var localVideo = document.querySelector('video#localvideo');
var remoteVideo = document.querySelector('video#remotevideo');
var btnConn = document.querySelector('button#connserver');
var btnLeave = document.querySelector('button#leave');
var offer = document.querySelector('textarea#offer');
var answer = document.querySelector('textarea#answer');
var shareDeskBox = document.querySelector('input#shareDesk');
var pcConfig = {
'iceServers': [{
'urls': 'turn:stun.al.learningrtc.cn:3478',
'credential': "mypasswd",
'username': "garrylea"
}]
};
var localStream = null;
var remoteStream = null;
var pc = null;
var roomid;
var socket = null;
var offerdesc = null;
var state = 'init';
// 以下代码是从网上找的
//=========================================================================================
//如果返回的是false说明当前操作系统是手机端,如果返回的是true则说明当前的操作系统是电脑端
function IsPC() {
var userAgentInfo = navigator.userAgent;
var Agents = ["Android", "iPhone","SymbianOS", "Windows Phone","iPad", "iPod"];
var flag = true;
for (var v = 0; v < Agents.length; v++) {
if (userAgentInfo.indexOf(Agents[v]) > 0) {
flag = false;
break;
}
}
return flag;
}
//如果返回true 则说明是Android false是ios
function is_android() {
var u = navigator.userAgent, app = navigator.appVersion;
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //g
var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
if (isAndroid) {
//这个是安卓操作系统
return true;
}
if (isIOS) {
//这个是ios操作系统
return false;
}
}
//获取url参数
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
//=======================================================================
function sendMessage(roomid, data){
console.log('send message to other end', roomid, data);
if(!socket){
console.log('socket is null');
}
socket.emit('message', roomid, data);
}
function conn(){
socket = io.connect();
socket.on('joined', (roomid, id) => {
console.log('receive joined message!', roomid, id);
state = 'joined'
//如果是多人的话,第一个人不该在这里创建peerConnection
//都等到收到一个otherjoin时再创建
//所以,在这个消息里应该带当前房间的用户数
//
//create conn and bind media track
createPeerConnection();
bindTracks();
btnConn.disabled = true;
btnLeave.disabled = false;
console.log('receive joined message, state=', state);
});
socket.on('otherjoin', (roomid) => {
console.log('receive joined message:', roomid, state);
//如果是多人的话,每上来一个人都要创建一个新的 peerConnection
//
if(state === 'joined_unbind'){
createPeerConnection();
bindTracks();
}
state = 'joined_conn';
call();
console.log('receive other_join message, state=', state);
});
socket.on('full', (roomid, id) => {
console.log('receive full message', roomid, id);
hangup();
closeLocalMedia();
state = 'leaved';
console.log('receive full message, state=', state);
alert('the room is full!');
});
socket.on('leaved', (roomid, id) => {
console.log('receive leaved message', roomid, id);
state='leaved'
socket.disconnect();
console.log('receive leaved message, state=', state);
btnConn.disabled = false;
btnLeave.disabled = true;
});
socket.on('bye', (room, id) => {
console.log('receive bye message', roomid, id);
//state = 'created';
//当是多人通话时,应该带上当前房间的用户数
//如果当前房间用户不小于 2, 则不用修改状态
//并且,关闭的应该是对应用户的peerconnection
//在客户端应该维护一张peerconnection表,它是
//一个key:value的格式,key=userid, value=peerconnection
state = 'joined_unbind';
hangup();
offer.value = '';
answer.value = '';
console.log('receive bye message, state=', state);
});
socket.on('disconnect', (socket) => {
console.log('receive disconnect message!', roomid);
if(!(state === 'leaved')){
hangup();
closeLocalMedia();
}
state = 'leaved';
});
socket.on('message', (roomid, data) => {
console.log('receive message!', roomid, data);
if(data === null || data === undefined){
console.error('the message is invalid!');
return;
}
if(data.hasOwnProperty('type') && data.type === 'offer') {
offer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
//create answer
pc.createAnswer()
.then(getAnswer)
.catch(handleAnswerError);
}else if(data.hasOwnProperty('type') && data.type == 'answer'){
answer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
}else if (data.hasOwnProperty('type') && data.type === 'candidate'){
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate: data.candidate
});
pc.addIceCandidate(candidate);
}else{
console.log('the message is invalid!', data);
}
});
roomid = getQueryVariable('room');
socket.emit('join', roomid);
return true;
}
function connSignalServer(){
//开启本地视频
start();
return true;
}
function getMediaStream(stream){
if(localStream){
stream.getAudioTracks().forEach((track)=>{
localStream.addTrack(track);
stream.removeTrack(track);
});
}else{
localStream = stream;
}
localVideo.srcObject = localStream;
//这个函数的位置特别重要,
//一定要放到getMediaStream之后再调用
//否则就会出现绑定失败的情况
//
//setup connection
conn();
//btnStart.disabled = true;
//btnCall.disabled = true;
//btnHangup.disabled = true;
}
function getDeskStream(stream){
localStream = stream;
}
function handleError(err){
console.error('Failed to get Media Stream!', err);
}
function shareDesk(){
if(IsPC()){
navigator.mediaDevices.getDisplayMedia({video: true})
.then(getDeskStream)
.catch(handleError);
return true;
}
return false;
}
function start(){
if(!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia){
console.error('the getUserMedia is not supported!');
return;
}else {
var constraints;
if( shareDeskBox.checked && shareDesk()){
constraints = {
video: false,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
}
}else{
constraints = {
video: true,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
}
}
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
}
}
function getRemoteStream(e){
remoteStream = e.streams[0];
remoteVideo.srcObject = e.streams[0];
}
function handleOfferError(err){
console.error('Failed to create offer:', err);
}
function handleAnswerError(err){
console.error('Failed to create answer:', err);
}
function getAnswer(desc){
pc.setLocalDescription(desc);
answer.value = desc.sdp;
//send answer sdp
sendMessage(roomid, desc);
}
function getOffer(desc){
pc.setLocalDescription(desc);
offer.value = desc.sdp;
offerdesc = desc;
//send offer sdp
sendMessage(roomid, offerdesc);
}
function createPeerConnection(){
//如果是多人的话,在这里要创建一个新的连接.
//新创建好的要放到一个map表中。
//key=userid, value=peerconnection
console.log('create RTCPeerConnection!');
if(!pc){
pc = new RTCPeerConnection(pcConfig);
pc.onicecandidate = (e)=>{
if(e.candidate) {
sendMessage(roomid, {
type: 'candidate',
label:event.candidate.sdpMLineIndex,
id:event.candidate.sdpMid,
candidate: event.candidate.candidate
});
}else{
console.log('this is the end candidate');
}
}
pc.ontrack = getRemoteStream;
}else {
console.warning('the pc have be created!');
}
return;
}
//绑定永远与 peerconnection在一起,
//所以没必要再单独做成一个函数
function bindTracks(){
console.log('bind tracks into RTCPeerConnection!');
if( pc === null || pc === undefined) {
console.error('pc is null or undefined!');
return;
}
if(localStream === null || localStream === undefined) {
console.error('localstream is null or undefined!');
return;
}
//add all track into peer connection
localStream.getTracks().forEach((track)=>{
pc.addTrack(track, localStream);
});
}
function call(){
if(state === 'joined_conn'){
var offerOptions = {
offerToRecieveAudio: 1,
offerToRecieveVideo: 1
}
pc.createOffer(offerOptions)
.then(getOffer)
.catch(handleOfferError);
}
}
function hangup(){
if(pc) {
offerdesc = null;
pc.close();
pc = null;
}
}
function closeLocalMedia(){
if(localStream && localStream.getTracks()){
localStream.getTracks().forEach((track)=>{
track.stop();
});
}
localStream = null;
}
function leave() {
if(socket){
socket.emit('leave', roomid); //notify server
}
hangup();
closeLocalMedia();
offer.value = '';
answer.value = '';
btnConn.disabled = false;
btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//main_signal.js
'use strict'
var localVideo = document.querySelector('video#localvideo');
var remoteVideo = document.querySelector('video#remotevideo');
var btnConn = document.querySelector('button#connserver');
var btnLeave = document.querySelector('button#leave');
var localStream = null;
var roomid;
var socket = null;
var state = 'init';
function sendMessage(roomid, data){
console.log('send message to other end', roomid, data);
if(!socket){
console.log('socket is null');
}
socket.emit('message', roomid, data);
}
function conn(){
socket = io.connect();
socket.on('joined', (roomid, id) => {
console.log('receive joined message!', roomid, id);
state = 'joined'
btnConn.disabled = true;
btnLeave.disabled = false;
console.log('receive joined message, state=', state);
});
socket.on('otherjoin', (roomid) => {
console.log('receive joined message:', roomid, state);
state = 'joined_conn';
console.log('receive other_join message, state=', state);
});
socket.on('full', (roomid, id) => {
console.log('receive full message', roomid, id);
state = 'leaved';
console.log('receive full message, state=', state);
alert('the room is full!');
});
socket.on('leaved', (roomid, id) => {
console.log('receive leaved message', roomid, id);
state='leaved'
btnConn.disabled = false;
btnLeave.disabled = true;
});
socket.on('bye', (room, id) => {
console.log('receive bye message', roomid, id);
state = 'joined_unbind';
console.log('receive bye message, state=', state);
});
socket.on('disconnect', (socket) => {
console.log('receive disconnect message!', roomid);
state = 'leaved';
console.log('receive disconnect message, state=', state);
});
socket.on('message', (roomid, data) => {
console.log('receive message!', roomid, data);
if(data === null || data === undefined){
console.error('the message is invalid!');
return;
}
if(data.hasOwnProperty('type') && data.type === 'offer') {
}else if(data.hasOwnProperty('type') && data.type == 'answer'){
}else if (data.hasOwnProperty('type') && data.type === 'candidate'){
}else{
console.log('the message is invalid!', data);
}
});
roomid = getQueryVariable('room');
socket.emit('join', roomid);
return true;
}
function connSignalServer(){
//开启本地视频
start();
return true;
}
function getMediaStream(stream){
localStream = stream;
localVideo.srcObject = localStream;
conn();
}
function handleError(err){
if(err){
console.error('Failed to get Media Stream:', err);
}else {
console.error('Failed to get Media Stream!', );
}
return;
}
function start(){
if(!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia){
console.error('the getUserMedia is not supported!');
return;
}else {
var constraints = {
video: true,
audio: false
}
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
}
}
function closeLocalMedia(){
if(localStream && localStream.getTracks()){
localStream.getTracks().forEach((track)=>{
track.stop();
});
}
localStream = null;
}
function leave() {
if(socket){
socket.emit('leave', roomid); //notify server
}
btnConn.disabled = false;
btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//main_sdp.js
'use strict'
var localVideo = document.querySelector('video#localvideo');
var remoteVideo = document.querySelector('video#remotevideo');
var btnConn = document.querySelector('button#connserver');
var btnLeave = document.querySelector('button#leave');
var pcConfig = {
'iceServers': [{
'urls': 'turn:stun.al.learningrtc.cn:3478',
'credential': "mypasswd",
'username': "garrylea"
}]
};
var localStream = null;
var remoteStream = null;
var pc = null;
var roomid;
var socket = null;
var offerdesc = null;
var state = 'init';
function conn(){
socket = io.connect();
socket.on('joined', (roomid, id) => {
console.log('receive joined message!', roomid, id);
state = 'joined'
createPeerConnection();
btnConn.disabled = true;
btnLeave.disabled = false;
console.log('receive joined message, state=', state);
});
socket.on('otherjoin', (roomid) => {
console.log('receive joined message:', roomid, state);
if(state === 'joined_unbind'){
createPeerConnection();
}
state = 'joined_conn';
call();
console.log('receive other_join message, state=', state);
});
socket.on('full', (roomid, id) => {
console.log('receive full message', roomid, id);
state = 'leaved';
console.log('receive full message, state=', state);
alert('the room is full!');
});
socket.on('leaved', (roomid, id) => {
console.log('receive leaved message', roomid, id);
state='leaved'
socket.disconnect();
console.log('receive leaved message, state=', state);
btnConn.disabled = false;
btnLeave.disabled = true;
});
socket.on('bye', (room, id) => {
console.log('receive bye message', roomid, id);
state = 'joined_unbind';
hangup();
console.log('receive bye message, state=', state);
});
socket.on('disconnect', (socket) => {
console.log('receive disconnect message!', roomid);
state = 'leaved';
console.log('receive disconnect message, state=', state);
});
socket.on('message', (roomid, data) => {
console.log('receive message!', roomid, data);
if(data === null || data === undefined){
console.error('the message is invalid!');
return;
}
if(data.hasOwnProperty('type') && data.type === 'offer') {
pc.setRemoteDescription(new RTCSessionDescription(data));
//create answer
pc.createAnswer()
.then(getAnswer)
.catch(handleAnswerError);
}else if(data.hasOwnProperty('type') && data.type == 'answer'){
answer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
}else if (data.hasOwnProperty('type') && data.type === 'candidate'){
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate: data.candidate
});
pc.addIceCandidate(candidate);
}else{
console.log('the message is invalid!', data);
}
});
roomid = getQueryVariable('room');
socket.emit('join', roomid);
return true;
}
function connSignalServer(){
//开启本地视频
start();
return true;
}
function getMediaStream(stream){
localStream = stream;
localVideo.srcObject = localStream;
//setup connection
conn();
}
function handleError(err){
if(err){
console.error('Failed to get Media Stream!', err);
}else {
console.error('Failed to get Media Stream!');
}
}
function start(){
if(!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia){
console.error('the getUserMedia is not supported!');
return;
}else {
var constraints = {
video: true,
audio: false
}
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
}
}
function getRemoteStream(e){
remoteStream = e.streams[0];
remoteVideo.srcObject = e.streams[0];
}
function sendMessage(roomid, data){
console.log('send message to other end', roomid, data);
if(!socket){
console.log('socket is null');
}
socket.emit('message', roomid, data);
}
function handleOfferError(err){
console.error('Failed to create offer:', err);
}
function handleAnswerError(err){
console.error('Failed to create answer:', err);
}
function getAnswer(desc){
pc.setLocalDescription(desc);
//send answer sdp
sendMessage(roomid, desc);
}
function getOffer(desc){
pc.setLocalDescription(desc);
sendMessage(roomid, offerdesc);
}
function call(){
if(state === 'joined_conn'){
var offerOptions = {
offerToRecieveAudio: 1,
offerToRecieveVideo: 1
}
pc.createOffer(offerOptions)
.then(getOffer)
.catch(handleOfferError);
}
}
function createPeerConnection(){
console.log('create RTCPeerConnection!');
if(!pc){
pc = new RTCPeerConnection(pcConfig);
pc.onicecandidate = (e)=>{
if(e.candidate) {
sendMessage(roomid, {
type: 'candidate',
label:event.candidate.sdpMLineIndex,
id:event.candidate.sdpMid,
candidate: event.candidate.candidate
});
}else{
console.log('this is the end candidate');
}
}
pc.ontrack = getRemoteStream;
}else {
console.warning('the pc have be created!');
}
console.log('bind tracks into RTCPeerConnection!');
if(localStream === null || localStream === undefined) {
console.error('localstream is null or undefined!');
return false;
}
//add all track into peer connection
localStream.getTracks().forEach((track)=>{
pc.addTrack(track, localStream);
});
return true;
}
function hangup(){
if(pc) {
pc.close();
pc = null;
}
}
function closeLocalMedia(){
if(localStream && localStream.getTracks()){
localStream.getTracks().forEach((track)=>{
track.stop();
});
}
localStream = null;
}
function leave() {
if(socket){
socket.emit('leave', roomid); //notify server
}
hangup();
closeLocalMedia();
btnConn.disabled = false;
btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//main_connection.js
'use strict'
var localVideo = document.querySelector('video#localvideo');
var remoteVideo = document.querySelector('video#remotevideo');
var btnConn = document.querySelector('button#connserver');
var btnLeave = document.querySelector('button#leave');
var pcConfig = {
'iceServers': [{
'urls': 'turn:stun.al.learningrtc.cn:3478',
'credential': "mypasswd",
'username': "garrylea"
}]
};
var localStream = null;
var remoteStream = null;
var pc = null;
var roomid;
var socket = null;
var state = 'init';
function conn(){
socket = io.connect();
socket.on('joined', (roomid, id) => {
console.log('receive joined message!', roomid, id);
state = 'joined'
createPeerConnection();
btnConn.disabled = true;
btnLeave.disabled = false;
console.log('receive joined message, state=', state);
});
socket.on('otherjoin', (roomid) => {
console.log('receive joined message:', roomid, state);
if(state === 'joined_unbind'){
createPeerConnection();
}
state = 'joined_conn';
call();
console.log('receive other_join message, state=', state);
});
socket.on('full', (roomid, id) => {
console.log('receive full message', roomid, id);
state = 'leaved';
console.log('receive full message, state=', state);
alert('the room is full!');
});
socket.on('leaved', (roomid, id) => {
console.log('receive leaved message', roomid, id);
state='leaved'
socket.disconnect();
console.log('receive leaved message, state=', state);
btnConn.disabled = false;
btnLeave.disabled = true;
});
socket.on('bye', (room, id) => {
console.log('receive bye message', roomid, id);
state = 'joined_unbind';
hangup();
console.log('receive bye message, state=', state);
});
socket.on('disconnect', (socket) => {
console.log('receive disconnect message!', roomid);
state = 'leaved';
console.log('receive disconnect message, state=', state);
});
socket.on('message', (roomid, data) => {
console.log('receive message!', roomid, data);
if(data === null || data === undefined){
console.error('the message is invalid!');
return;
}
if(data.hasOwnProperty('type') && data.type === 'offer') {
}else if(data.hasOwnProperty('type') && data.type == 'answer'){
}else if (data.hasOwnProperty('type') && data.type === 'candidate'){
}else{
console.log('the message is invalid!', data);
}
});
roomid = getQueryVariable('room');
socket.emit('join', roomid);
return true;
}
function connSignalServer(){
//开启本地视频
start();
return true;
}
function getMediaStream(stream){
localStream = stream;
localVideo.srcObject = localStream;
//setup connection
conn();
}
function handleError(err){
if(err){
console.error('Failed to get Media Stream!', err);
}else {
console.error('Failed to get Media Stream!');
}
}
function start(){
if(!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia){
console.error('the getUserMedia is not supported!');
return;
}else {
var constraints = {
video: true,
audio: false
}
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
}
}
function getRemoteStream(e){
remoteStream = e.streams[0];
remoteVideo.srcObject = e.streams[0];
}
function createPeerConnection(){
console.log('create RTCPeerConnection!');
if(!pc){
pc = new RTCPeerConnection(pcConfig);
pc.onicecandidate = (e)=>{
if(e.candidate) {
sendMessage(roomid, {
type: 'candidate',
label:event.candidate.sdpMLineIndex,
id:event.candidate.sdpMid,
candidate: event.candidate.candidate
});
}else{
console.log('this is the end candidate');
}
}
pc.ontrack = getRemoteStream;
}else {
console.warning('the pc have be created!');
}
console.log('bind tracks into RTCPeerConnection!');
if(localStream === null || localStream === undefined) {
console.error('localstream is null or undefined!');
return false;
}
//add all track into peer connection
localStream.getTracks().forEach((track)=>{
pc.addTrack(track, localStream);
});
return true;
}
function call(){
if(state === 'joined_conn'){
var offerOptions = {
offerToRecieveAudio: 1,
offerToRecieveVideo: 1
}
pc.createOffer(offerOptions)
.then(getOffer)
.catch(handleOfferError);
}
}
function hangup(){
if(pc) {
pc.close();
pc = null;
}
}
function closeLocalMedia(){
if(localStream && localStream.getTracks()){
localStream.getTracks().forEach((track)=>{
track.stop();
});
}
localStream = null;
}
function sendMessage(roomid, data){
console.log('send message to other end', roomid, data);
if(!socket){
console.log('socket is null');
}
socket.emit('message', roomid, data);
}
function leave() {
if(socket){
socket.emit('leave', roomid); //notify server
}
hangup();
closeLocalMedia();
btnConn.disabled = false;
btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//index.html
<html>
<head>
<title>really peer connection</title>
<link rel="stylesheet" href="./css/main.css">
<script language="javascript" type="text/javascript">
function gotoNextPage(){
var roomid = document.querySelector('input#room');
if(roomid.value === null || roomid.value === ''){
alert('roomid is null');
}else {
window.location.href="room.html?room="+ roomid.value;
}
}
</script>
</head>
<body>
<table align="center">
<tr><td><div>
<label>roomid:</label>
<input type="input" id="room">
</div></td></tr>
<tr><td><div>
<button id="join" onclick="gotoNextPage()">Join</button>
</div></td></tr>
</table>
</body>
</html>
//room.html
<html>
<head>
<title>WebRTC PeerConnection</title>
<link href="./css/main.css" rel="stylesheet" />
</head>
<body>
<div>
<div>
<button id="connserver">Connect Sig Server</button>
<!--<button id="start" disabled>Start</button>
<button id="call" disabled>Call</button>
<button id="hangup" disabled>HangUp</button>
-->
<button id="leave" disabled>Leave</button>
</div>
<div>
<input id="shareDesk" type="checkbox"/><label for="shareDesk">Share Desktop</label>
</div>
<div id="preview">
<div >
<h2>Local:</h2>
<video id="localvideo" autoplay playsinline muted></video>
<h2>Offer SDP:</h2>
<textarea id="offer"></textarea>
</div>
<div>
<h2>Remote:</h2>
<video id="remotevideo" autoplay playsinline></video>
<h2>Answer SDP:</h2>
<textarea id="answer"></textarea>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
如果远程共享桌面
//只是提几个重要api
var promise=navigator.mediaDevices.getDisplayMedia(contraints);
//constraints可选
//constraints中约束与getUserMedia函数中的基本一致
- getDisplayMedia无法同时采集音频和桌面,一般都是采集桌面
- 桌面采集时候分辨率是否可调整
总结
- 网络连接要在音视频数据获取到之后,否则有可能绑定音视频失败
- 当一段退出房间后,另一端的PeerConnection要关闭重建,否则与新用户互通时媒体协商会失败
- 异步事件处理