原创

P2P通信 - P2P媒体协商通讯基本流程及WEB视频聊天与桌面共享示例

项目地址:https://github.com/phpisfuture/webrtc-web-android-ios-video-chat-demo

媒体协商过程

file

1、发起方Amy创建Offer的SDP,调用setLocalDescription方法设置自身信息并收集Candidate候选者,发送Offer信息到信令服务器。
2、接收方Bob收到信令服务器发送过来的Offer,调用setRemoteDescription方法设置远端的相关信息,然后创建Answer的SDP,调用setLocalDescription方法设置自信信息并收集Candidate候选者,发送Answer信息到信令服务器。
3、发起方Amy收到信令服务器发送过来的Answer,调用setRemoteDescription方法设置远端的相关信息。

P2P链接的基本流程

file

注意:

  • 要先获取流并绑定到轨道中在进行信令交换
  • 当一端退出房间后,另一端的peerConnection也要关闭重建,否则新用户协商会失败

1、发起端A与被呼叫端B与信令服务器建立链接。
2、发起端A创建RTCPeerConnection并绑定onicecandidate与ontrack事件,通过getUserMedia、getMediaStream拿到本地的音视频流,调用addTrack将流添加到RTCPeerConnection轨道中。
3、发起端A通过createOffer方法创建Offer的SDP,调用setLocalDescription方法设置自身信息并通过ice框架stun与turn服务发送bind request收集Candidate候选者.
4、发起端A拿到Candidate候选者发送Offer SDP到信令服务器,信令服务器中转Offer SDP到被呼叫端B。
5、被呼叫端B收到信令服务器发来的Offer SDP后首先创建RTCPeerConnection,创建RTCSessionDescription并调用setRemoteDescription方法设置远端的相关信息,然后创建Answer SDP,调用setLocalDescription方法设置自信信息并通过ice框架stun与turn服务发送bind request收集Candidate候选者,发送Answer信息到信令服务器。
6、发起端A收到信令服务器中转来的Answer SDP后,创建RTCSessionDescription并调用setRemoteDescription方法设置远端的相关信息,这时就会收到被呼叫端B的Candidate信息,并触发stun/turn服务的onicecandidate事件。
7、发起端A收到onicecandidate事件发送Candidate到信令服务器,信令服务器将Candidate信息转发给被呼叫端B。被呼叫端B收到信令服务器发送来的Candidate信息后创建RTCIceCandidate,调用addIceCandidate方法将Candidate信息添加到候选者列表中。
8、同时被呼叫端B收到onicecandidate事件发送Candidate到信令服务器,信令服务器将Candidate信息转发给发起端A。发起端A收到信令服务器发送来的Candidate信息后创建RTCIceCandidate,调用addIceCandidate方法将Candidate信息添加到候选者列表中。
9、此时双放拿到了对方所有可能的候选者通路,测试底层进行排序及连通性检测,找到最优线路,并发送视频流数据。
10、此时对方通过ontrack事件获取远程音视频流进行展示。

状态逻辑切换

state状态取值:

  • init:初始化状态
  • joined:加入房间
  • joined_conn:加入并链接
  • joined_unbind:链接后挂断
  • leaved:离开房间

例子

peer端(html):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>p2pdemo</title>
</head>
<body>
    <div>
        <div>
            <div>
                <label>local</label>
                <video id="localVideo" autoplay playsinline></video>
            </div>
            <div>
                <lable>remote</lable>
                <video id="remoteVideo" autoplay playsinline></video>
            </div>
        </div>
        <div>
            <input id="shareDesk" type="checkbox"/><label for="shareDesk">Share Desktop</label>
        </div>
        <div>
            <button id="online">上线</button>
            <button id="call">呼叫</button>
            <button id="leave">挂断</button>
            <button id="offline">下线</button>
        </div>
    </div>
</body>
<script src="js/main.js"></script>
<script src="js/socket.io.js"></script>
</html>

peer端(JavaScript):

var localVideo = document.querySelector('video#localVideo');
var remoteVideo = document.querySelector('video#remoteVideo');

var shareDeskBox = document.querySelector('input#shareDesk');

var btnCall = document.querySelector('button#call');
var btnLeave = document.querySelector('button#leave');
var btnOnline = document.querySelector('button#online');
var btnOffline = document.querySelector('button#offline');

// 信令服务器socket
var socket;

// peerConnection
var peerConnection;

// 本地音视频流
var localStream;

// 远程音视频流
var remoteStream;

// 房间ID
var roomId = "123";

var state = 'init';

// peerConnection配置
const peerConnectionConfigs = {
    'iceServers': [{
        'urls': 'turn:webrtc.phpisfuture.com:3478',
        'credential': "weilai",
        'username': "123456"
    }],
};

// 音视频呼叫
function call() {
    getLocalMedia();
}

// 媒体协商发送offer
function sendOffer(roomId) {
    if (state === 'joined_conn') {
        var offerOptions = {
            offerToReceiveVideo: 1,
            offerToReceiveAudio: 1
        };

        // 创建offer
        peerConnection.createOffer(offerOptions)
            .then(function (desc) {
                // 设置自身信息并通过ice框架stun与turn服务发送bind request收集Candidate候选者
                peerConnection.setLocalDescription(desc);
                console.log("offer type and sdp", desc);
                // 发送offer sdp信息到信令服务器
                sendMessage(roomId, desc);
            })
            .catch(function (error) {
                console.error('Failed to create offer:', error);
            });
    }
}

// 媒体协商发送answer
function sendAnswer(roomId) {
    var answerOptions = {
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 1
    };
    // 创建answer
    peerConnection.createAnswer(answerOptions)
        .then(function (desc) {
            // 设置自身信息并通过ice框架stun与turn服务发送bind request收集Candidate候选者
            peerConnection.setLocalDescription(desc);
            console.log("answer type and sdp", desc);
            // 发送answer sdp信息到信令服务器
            sendMessage(roomId, desc);
        })
        .catch(function (error) {
            console.error('Failed to create answer:', error);
        });
}

// 创建createPeerConnection并绑定事件
function createPeerConnection(roomId) {
    console.log('create RTCPeerConnection!');
    peerConnection = new RTCPeerConnection(peerConnectionConfigs);
    // onicecandidate事件处理(当收集到候选者时,发送candidate到信令服务器)
    peerConnection.onicecandidate = function (iceCandidate) {
        if (iceCandidate.candidate) {
            sendMessage(roomId, {
                type: 'candidate',
                sdpMLineIndex: iceCandidate.candidate.sdpMLineIndex,
                sdpMid: iceCandidate.candidate.sdpMid,
                candidate: iceCandidate.candidate.candidate
            })
        } else {
            console.log('this is the end candidate');
        }
    };
    // ontrack事件处理(当收到媒体轨数据时)
    peerConnection.ontrack = function (e) {
        remoteStream = e.streams[0];
        remoteVideo.srcObject = remoteStream;
    };
}

// 绑定媒体轨
function bindTracks() {
    console.log('bind tracks into RTCPeerConnection!');

    if (peerConnection === null || peerConnection === undefined) {
        console.error('pc is null or undefined!');
        return;
    }

    if (localStream === null || localStream === undefined) {
        console.error('localstream is null or undefined!');
        return;
    }

    // 添加所有需要的轨道(可以筛选必要的轨道)到peerConnection(注意要先绑定媒体轨,在进行媒体协商)
    localStream.getTracks().forEach((track) => {
        peerConnection.addTrack(track, localStream);
    });
}

// 绑定信令处理,并发送加入消息
function signal() {

    // 自己加入成功
    socket.on("joined", function (roomId, socketId) {
        console.log('receive joined message!', roomId, socketId);
        state = 'joined';
        // 创建createPeerConnection并绑定事件
        createPeerConnection(roomId);
        // 绑定媒体轨
        bindTracks();
    });

    // 其他人加入
    socket.on("otherjoin", function (roomId) {

        // 如果对方挂断后需要重新创建peerConnection
        if (state === 'joined_unbind') {
            createPeerConnection();
            bindTracks();
        }
        state = 'joined_conn';
        // 发送offer
        sendOffer(roomId);
    });

    // 房间已满
    socket.on('full', (roomId, socketId) => {
        console.log('receive full message', roomId, socketId);
        state = 'leaved';
        offline();
        alert('the room is full!');
    });

    // 离开房间
    socket.on("leaved", function (roomId, socketId) {
        console.log('receive leaved message', roomId, socketId);
        state = 'leaved';
        socket.disconnect();
    });

    // 如果对方离开
    socket.on("bye", function (roomId, socketId) {
        console.log('receive bye message', roomId, socketId);
        state = 'joined_unbind';
        peerConnection.close();
    });

    // 断开链接
    socket.on('disconnect', (socket) => {
        console.log('receive disconnect message!', socket.id);
        if (!(state === 'leaved')) {
            offline();
        }
        state = 'leaved';
    });

    // 收到消息
    socket.on("message", function (roomId, data) {
        console.log('receive message!', roomId, data);
        if (data === null || data === undefined) {
            console.error('the message is invalid!');
            return;
        }
        // 收到offer
        if (data.hasOwnProperty('type') && data.type === 'offer') {
            peerConnection.setRemoteDescription(new RTCSessionDescription(data));
            sendAnswer(roomId);
        }
        // 收到answer
        else if (data.hasOwnProperty('type') && data.type === 'answer') {
            peerConnection.setRemoteDescription(new RTCSessionDescription(data));
        }
        // 收到 candidate
        else if (data.hasOwnProperty('type') && data.type === 'candidate') {
            var candidate = new RTCIceCandidate({
                sdpMLineIndex: data.sdpMLineIndex,
                sdpMid: data.sdpMid,
                candidate: data.candidate
            });
            // 添加candidate到peerConnection候选者列表中
            peerConnection.addIceCandidate(candidate);
        }
    });

    // 发送加入消息
    socket.emit("join", roomId);
}

//如果返回的是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;
}

function shareDesk() {

    if (IsPC()) {
        navigator.mediaDevices.getDisplayMedia({video: true})
            .then(function (stream) {
                localStream = stream;
            })
            .catch(function (error) {
                alert('Failed to get Media Stream!' + error);
            });

        return true;
    }

    return false;

}

// 获取媒体流
function getLocalMedia() {
    if (!navigator.mediaDevices ||
        !navigator.mediaDevices.getUserMedia) {
        console.error('the getUserMedia is not supported!');
    } else {
        var constraints;
        // 如果分享屏幕
        if (shareDeskBox.checked && shareDesk()) {
            constraints = {
                video: false,
                audio: {
                    echoCancellation: true,
                    noiseSuppression: true,
                    autoGainControl: false
                }
            }
        } else {
            constraints = {
                video: true,
                audio: {
                    echoCancellation: true,
                    noiseSuppression: true,
                    autoGainControl: false
                }
            }
        }
        // 获取媒体流
        navigator.mediaDevices.getUserMedia(constraints)
            .then(function (mediaStream) {
                // 如果本地流存在(是桌面采集流)
                if (localStream) {
                    // 将音频流添加到媒体轨中
                    mediaStream.getAudioTracks().forEach((track) => {
                        localStream.addTrack(track);
                        // 移除采集到的mediaStream媒体轨
                        mediaStream.removeTrack(track);
                    });
                } else {
                    // 储存本地媒体流信息
                    localStream = mediaStream;
                }
                // 绑定信令处理,并发送加入消息
                signal();
                localVideo.srcObject = localStream;
            })
            .catch(function (error) {
                alert('Failed to get Media Stream!' + error);
            });
    }
}

// 发送消息到信令服务器
function sendMessage(roomId, data) {
    console.log('send message to other end', roomId, data);
    if (!socket) {
        console.log('socket is reconnect');
        online();
        socket.emit("join", roomId);
    }
    socket.emit('message', roomId, data);
}


// 挂断
function leave() {
    if (socket) {
        // 发送离开消息
        socket.emit('leave', roomId); //notify server
    }

    if (peerConnection) {
        // 关闭链接
        peerConnection.close();
        peerConnection = null;
    }

    if (localStream && localStream.getTracks()) {
        // 停止本地音视频流轨
        localStream.getTracks().forEach((track) => {
            track.stop();
        });
    }

    localStream = null;

    alert("leave success");
}

// 上线
function online() {
    if (!socket) {
        // 链接信令服务器
        socket = io.connect();
        alert("online success");
    }
}

// 下线
function offline() {
    leave();
    if (socket) {
        // 断开信令服务器
        socket.disconnect();
        socket = null;
        alert("offline success");
    }
}

btnOnline.onclick = online;
btnCall.onclick = call;
btnLeave.onclick = leave;
btnOffline.onclick = offline;

信令服务器(nodeJS):

'use strict';

var http = require('http');
var https = require('https');
var fs = require('fs');

var express = require('express');
var serveIndex = require('serve-index');

//socket.io
var socketIo = require('socket.io');

//
var log4js = require('log4js');

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

const USERCOUNT = 3;

var app = express();
app.use(serveIndex('./public'));
app.use(express.static('./public'));

//http server
var http_server = http.createServer(app);
http_server.listen(8888, '0.0.0.0');

var options = {
    key: fs.readFileSync('./cert/2_webrtc.phpisfuture.com.key'),
    cert: fs.readFileSync('./cert/1_webrtc.phpisfuture.com_bundle.crt')
};
//https server
var https_server = https.createServer(options, app);
var io = socketIo.listen(https_server);

io.sockets.on('connection', (socket)=> {

    // 收到message消息
    socket.on('message', (room, data)=>{
        // 给room房间所有人除了自己(发送者)发送消息
        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){
                // 给room房间所有人除了自己(发送者)发送otherjoin消息
                socket.to(room).emit('otherjoin', room, socket.id);
            }
        }else{
            socket.leave(room);
            // 给自己(发送者)发送full消息
            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);
        // 给room房间所有人除了自己(发送者)发送bye消息
        socket.to(room).emit('bye', room, socket.id);
        // 给自己(发送者)发送leaved消息
        socket.emit('leaved', room, socket.id);
        //io.in(room).emit('leaved', room, socket.id);
    });

});

https_server.listen(4433, '0.0.0.0');

ICE STUN/TURN服务

使用coturn搭建,github地址:https://github.com/coturn/coturn
搭建教程,centOS安装coturn搭建webRTC的STUN/TURN服务器

正文到此结束
本文目录