webRTC-简易视频通讯服务器

webRTC-简易视频通讯服务器 目录


前言

  • 本文基于webSocket建立了一个网络视频通讯

推荐阅读

  • 《Learning WebRTC》

涵盖内容

  • 从客户端获取到服务器的连接
  • 识别各个连接端的用户
  • 两个远程用户发起通话
  • 结束通话

展示效果

在这里插入图片描述

连接

  • 包含两个页面:输入用户名,呼叫其他用户

创建页面

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    body{
     
     
        background-color: #3D6DF2;
        margin-top: 15px;
        font-family: sans-serif;
        color: white;
    }

    video {
     
     
        background: black;
        border: 1px solid gray;
    }

    .page {
     
     
        position: relative;
        display: block;
        margin: 0 auto;
        width: 500px;
        height: 500px;
    }

    #yours{
     
     
        width: 150px;
        height: 150px;
        position: absolute;
        top: 15px;
        right: 15px;
    }

    #theirs {
     
     
        width: 500px;
        height: 500px;
    }
</style>
<body>
    <div id="login-page" class="page">
        <h2>Login As</h2>
        <input type="text" id="username">
        <button id="login">Login</button>
    </div>

    <div id="call-page" class="page">
        <video id="yours" autoplay></video>
        <video id="theirs" autoplay></video>

        <input type="text" id="their-username">
        <button id="call">Call</button>
        <button id="hang-up">Hang Up</button>
    </div>
</body>
<script src="client.js"></script>
</html>

获取一个连接

  • 与信令服务器创建连接
let name,
    connectedUser;

let connection = new WebSocket('ws://localhost:8888');

connection.onopen = function() {
    
    
    console.log("Connected");
};

connection.onmessage = function(message){
    
    
    console.log("Got message", message.data);

    let data = JSON.parse(message.data);

    switch(data.type){
    
    
        case "login":
            onLogin(data.success);
            break;
        case "offer":
            onOffer(data.offer, data.name);
            break;
        case "answer":
            onAnswer(data.answer);
            break;
        case "candidate":
            onCandidate(data.candidate);
            break;
        case "leave":
            onLeave();
            break;
        default:
            break;
    }
};

connection.onerror = function(err) {
    
    
    console.log(err);
}

function send(message) {
    
    
    if(connectedUser) {
    
    
        message.name = connectedUser;
    }

    connection.send(JSON.stringify(message));
}

登录

let loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    callPage = document.querySelector('#call-page'),
    theirUsernameInput = document.querySelector('#theirusername'),
    callButton = document.querySelector('#call'),
    hangUpButton = document.querySelector('#hang-up');

callPage.style.display = 'none';

loginButton.addEventListener('click', event=>{
    
    
    name = usernameInput.value;

    if(name.length > 0){
    
    
        send({
    
    
            type: "login",
            name: name
        });
    }
});

function onLogin(success) {
    
    
    if(success === false){
    
    
        alert("Login unsuccessfully, please try a different name.");
    }else {
    
    
        loginPage.style.display = 'none';
        callPage.style.display = 'block';
    }

    // 准备好通话的通道
    startConnection();

}

开始对等连接

  1. 从相机中获取视频流
  2. 验证用户的浏览器是否支持WebRTC
  3. 创建RTCPeerConnection对象
let yourVideo = document.querySelector('#yours'),
    theirVideo = document.querySelector('#theirs'),
    yourConnection,
    connectedUser,
    stream;

function startConnection() {
    
    
    if(hasUserMedia()){
    
    
        navigator.getUserMedia({
    
    
            video: true,
            audio: true
        },
        myStream => {
    
    
            stream = myStream;
            
            try{
    
    
                yourVideo.src = window.URL.createObjectURL(stream);
            }catch(e){
    
    
                yourVideo.srcObject = stream;
            }

            if(hasRTCPeerConnection()){
    
    
                setupPeerConnection(stream);
            }else{
    
    
                alert("不支持webRTC");
            }
        },
        error => {
    
    
            console.log(error);
        }
        );
    }else{
    
    
        alert("不支持webRTC");
    }
}

function setupPeerConnection(stream) {
    
    
    let configuration = {
    
    
        "iceServers":[
            {
    
    "url":"stun:localhost:8888"}
        ]
    };

    yourConnection = new RTCPeerConnection(configuration);

    // 设置流量监听
    yourConnection.addStream(stream);
    yourConnection.onaddstream = function(e) {
    
    
        try{
    
    
            theirVideo.src = window.URL.createObjectURL(e.stream);
        }catch(e){
    
    
            theirVideo.srcObject = stream;
        }
    };

    yourConnection.onicecandidate = function(event) {
    
    
        if(event.candidate){
    
    
            send({
    
    
                type: "candidate",
                candidate: event.candidate
            });
        }
    };
}

function hasUserMedia() {
    
    
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    
    
    window.RTCPeerConnection = window.RTCPeerConnection
                            || window.webkitRTCPeerConnection
                            || window.mozRTCPeerConnection;

    window.RTCSessionDescription = window.RTCSessionDescription
                            || window.webkitRTCSessionDescription
                            || window.mozRTCSessionDescription;
    
    window.RTCIceCandidate = window.RTCIceCandidate
                            || window.webkitRTCIceCandidate
                            || window.mozeRTCIceCandidate;

    return !!window.RTCPeerConnection;
}

发起通话

  • setLocalDescription():更改与连接关联的本地描述。此说明指定连接的本地端的属性,包括媒体格式。
  • setRemoteDescription():将指定的会话描述设置为远程对等方的当前提供或应答。描述指定连接远端的属性,包括媒体格式。
  • addIceCandidate():通过信令信道从远程对等方接收新的ICE候选,它通过调用将新接收的候选发送到浏览器的ICE代理
callButton.addEventListener('click', function(){
    
    
    let theirUsername = theirUsernameInput.value;

    if(theirUsername.length > 0){
    
    
        startPeerConnection(theirUsername);
    }
});

function startPeerConnection(user) {
    
    
    connectedUser = user;

    // offer
    yourConnection.createOffer(offer=>{
    
    
        send({
    
    
            type: "offer",
            offer: offer
        });
        yourConnection.setLocalDescription(offer);
    },
    err => {
    
    
        alert("An error has occurred");
    });
}

function onOffer(offer, name) {
    
    
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(function(answer){
    
    
        yourConnection.setLocalDescription(answer);

        send({
    
    
            type: "answer",
            answer: answer
        });
    },
    err =>{
    
    
        alert("An error");
    }
    );
}

function onAnswer(answer){
    
    
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

function onCandidate(candidate){
    
    
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}

检测通信

  • 调试实时应用困难在:许多事件发生在同一时刻,要完整描述某一时刻发生了什么很难

  • ChromeView->Developer->Developer Tools可以看到webSocket得通信状态

挂断电话

  • 通知其他用户关闭通话
  • 销毁本地连接,允许进行新的通话

过程

  1. 通知服务器,断开连接
  2. 通知RTCPeerConnection关闭,停止发送数据流给其他用户
  3. 再次设置,连接实例设置为打开状态,以接受新的通话
hangUpButton.addEventListener("click", ()=>{
    
    
    send({
    
    
        type: "leave"
    });

    onLeave();
});

function onLeave() {
    
    
    connectedUser = null;
    theirVideo.srcObject = null;
    yourConnection.close();
    yourConnection.onicecandidate = null;
    yourConnection.onaddstream = null;
    setupPeerConnection(stream);
}

webRTC全部代码总结

  • 全部代码整合
// 变量声明
let name,
    connectedUser;

let connection = new WebSocket('ws://localhost:8888');

let yourVideo = document.querySelector('#yours'),
    theirVideo = document.querySelector('#theirs'),
    yourConnection,
    stream;

let loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    callPage = document.querySelector('#call-page'),
    theirUsernameInput = document.querySelector('#their-username'),
    callButton = document.querySelector('#call'),
    hangUpButton = document.querySelector('#hang-up');

callPage.style.display = 'none';

// 点击按钮登录
loginButton.addEventListener('click', event=>{
    
    
    name = usernameInput.value;

    if(name.length > 0){
    
    
        send({
    
    
            type: "login",
            name: name
        });
    }
});

// websocket 连接
connection.onopen = function() {
    
    
    console.log("Connected");
};

// 监听websocket信息
connection.onmessage = function(message){
    
    
    console.log("Got message", message.data);

    let data = JSON.parse(message.data);

    switch(data.type){
    
    
        case "login":
            onLogin(data.success);
            break;
        case "offer":
            onOffer(data.offer, data.name);
            break;
        case "answer":
            onAnswer(data.answer);
            break;
        case "candidate":
            onCandidate(data.candidate);
            break;
        case "leave":
            onLeave();
            break;
        default:
            break;
    }
};

// websocket报错信息
connection.onerror = function(err) {
    
    
    console.log(err);
}

// Alia 以JSON格式发送信息
function send(message) {
    
    
    if(connectedUser) {
    
    
        message.name = connectedUser;
    }

    connection.send(JSON.stringify(message));
}

function onLogin(success) {
    
    
    if(success === false){
    
    
        alert("Login unsuccessfully, please try a different name.");
    }else {
    
    
        loginPage.style.display = 'none';
        callPage.style.display = 'block';
    }

    // 准备好通话的通道
    startConnection();
}

// call呼叫
callButton.addEventListener('click', function(){
    
    
    let theirUsername = theirUsernameInput.value;

    if(theirUsername.length > 0){
    
    
        startPeerConnection(theirUsername);
    }
});

// 挂断
hangUpButton.addEventListener("click", ()=>{
    
    
    send({
    
    
        type: "leave"
    });

    onLeave();
});

function onOffer(offer, name) {
    
    
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(function(answer){
    
    
        yourConnection.setLocalDescription(answer);

        send({
    
    
            type: "answer",
            answer: answer
        });
    },
    err =>{
    
    
        alert("An error");
    }
    );
}

function onAnswer(answer){
    
    
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

function onCandidate(candidate){
    
    
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}


function onLeave() {
    
    
    connectedUser = null;
    theirVideo.srcObject = null;
    yourConnection.close();
    yourConnection.onicecandidate = null;
    yourConnection.onaddstream = null;
    setupPeerConnection(stream);
}


// 函数的polyfill
function hasUserMedia() {
    
    
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    
    
    window.RTCPeerConnection = window.RTCPeerConnection
                            || window.webkitRTCPeerConnection
                            || window.mozRTCPeerConnection;

    window.RTCSessionDescription = window.RTCSessionDescription
                            || window.webkitRTCSessionDescription
                            || window.mozRTCSessionDescription;
    
    window.RTCIceCandidate = window.RTCIceCandidate
                            || window.webkitRTCIceCandidate
                            || window.mozeRTCIceCandidate;

    return !!window.RTCPeerConnection;
}

// 开始连接
function startConnection() {
    
    
    if(hasUserMedia()){
    
    
        navigator.getUserMedia({
    
    
            video: true,
            audio: false
        },
        myStream => {
    
    
            stream = myStream;
            
            try{
    
    
                yourVideo.src = window.URL.createObjectURL(stream);
            }catch(e){
    
    
                yourVideo.srcObject = stream;
            }

            if(hasRTCPeerConnection()){
    
    
                setupPeerConnection(stream);
            }else{
    
    
                alert("不支持webRTC");
            }
        },
        error => {
    
    
            console.log(error);
        }
        );
    }else{
    
    
        alert("不支持webRTC");
    }
}

// 
function setupPeerConnection(stream) {
    
    
    let configuration = {
    
    
        "iceServers":[
            {
    
    "url":"stun:localhost:8888"}
        ]
    };

    yourConnection = new RTCPeerConnection(configuration);

    // 设置流量监听
    yourConnection.addStream(stream);
    yourConnection.onaddstream = function(e) {
    
    
        try{
    
    
            theirVideo.src = window.URL.createObjectURL(e.stream);
        }catch(e){
    
    
            theirVideo.srcObject = stream;
        }
    };
    // 设置ICE处理事件
    yourConnection.onicecandidate = function(event) {
    
    
        if(event.candidate){
    
    
            send({
    
    
                type: "candidate",
                candidate: event.candidate
            });
        }
    };
}

// 开始创建offer
function startPeerConnection(user) {
    
    
    connectedUser = user;

    // offer
    yourConnection.createOffer(offer=>{
    
    
        send({
    
    
            type: "offer",
            offer: offer
        });
        yourConnection.setLocalDescription(offer);
    },
    err => {
    
    
        alert("An error has occurred");
    });
}

完整的信令服务器

const WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({
    
    port: 8888}),
    users = {
    
    };

wss.on("connection", connection => {
    
    
    console.log("User connected");

    connection.on("message", message => {
    
    
        // console.log("Got message:", message);
        let data, conn;

        try{
    
    
            data = JSON.parse(message);
        }catch(e) {
    
    
            console.log(e);
            data = {
    
    };
        }


        switch(data.type) {
    
    
            case "login":
                console.log("User logged in as", data.name);
                if(users[data.name]) {
    
    
                    sendTo(connection, {
    
    
                        type: "login",
                        success: false
                    });
                }else {
    
    
                    users[data.name] = connection;
                    connection.name = data.name;
                    sendTo(connection, {
    
    
                        type: "login",
                        success: true
                    });
                }
                break;
            
            case "offer":
                console.log("sending offer to:", data.name);
                conn = users[data.name];

                if(conn != null){
    
    
                    connection.otherName = data.name;
                    sendTo(conn, {
    
    
                        type: "offer",
                        offer: data.offer,
                        name: connection.name
                    });
                }
                break;

            case "answer":
                console.log("sending answer to:", data.name);
                conn = users[data.name];

                if(conn != null){
    
    
                    connection.otherName = data.name;
                    sendTo(conn, {
    
    
                        type: "answer",
                        answer: data.answer
                    })
                }
                break;

            case "candidate":
                console.log("sending to", data.name);
                conn = users[data.name];

                if(conn != null){
    
    
                    sendTo(conn, {
    
    
                        type: "candidate",
                        candidate: data.candidate
                    });
                }
                break;
            
            case "leave":
                console.log("Disconnected user from ", data.name);
                conn = users[data.name];
                conn.otherName = null;

                if(conn != null){
    
    
                    sendTo(conn, {
    
    
                        type: "leave"
                    });
                }
                break;
                
            default:
                sendTo(connection, {
    
    
                    type: "error",
                    message: "Unrecognized command: " + data.type
                });

                break;
        }
    });

});

wss.on("close", function(){
    
    
    if(connection.name){
    
    
        delete users[connection.name];

        if(connection.otherName) {
    
    
            console.log("Disconnected,",connection.otherName);
            let conn = users[connection.otherName];
            conn.otherName = null;

            if(conn != null){
    
    
                sendTo(conn,{
    
    
                    type: "leave"
                });
            }
        }
    }
});

wss.on("listening", () => {
    
    
    console.log("Server started...");
});

function sendTo(conn, message) {
    
    
    conn.send(JSON.stringify(message));
}

猜你喜欢

转载自blog.csdn.net/u013362192/article/details/115154876