从小白到入门webrtc音视频通话

0. 写在前面

先会骑车,再研究为什么这么骑,才是我认为学习技术的思路,底部付了demo例子,根据例子上面的介绍即可运行。

1. 音视频通话要用到的技术简介

  1. websocket
    • 介绍:1. 服务器可以向浏览器推送信息;2. 一次握手成功,可持续互相发送信息
    • 在音视频通话钟的作用:1. 作为音视频两个通话终端的桥梁,传递彼此上下线、网络环境等消息,因此他们都叫websocket为“信令服务器”
  2. coturn
    • 介绍:1. 包含stun服务和turn服务,stun可实现两个终端点对点语音通话;turn服务在无法点对点通话时,用作中转音视频流。
  3. webrtc
    • 介绍:1. 开源项目;2. 用于音视频实时互动、游戏、即时通讯、文件传输。

2. webrtc音视频通话开发思路

2.1. webrtc调用时序图

  1. 下图简化了B客户端创建PeerConnection,具体流程要看下面“调用时序图介绍”
    webrtc调用时序图

2.2. 调用时序图介绍

  1. 上图名词介绍
    1. client A:客户端A
    2. Stun Server:穿透服务器,也就是coturn服务器中的Stun
    3. Signal Server:信令服务器,也就是web socket搭建的服务器
    4. client B:客户端B
    5. PeerConnection(WebRtc的接口)
  2. 流程介绍
    1. A客户端先发送信息到信令服务器,信令服务器存储A客户端信息,等待其他客户端加入。
    2. B客户端再发送信息到信令服务器,信令服务器存储B客户端信息,并告知A已有新客户端加入。
    3. A客户端创建 PeerConnection(WebRtc的接口),用于获取本地的网络信息、以及保存对方的网络信息、传递音视频流、监听通话过程状态。(B客户端后面也需要创建PeerConnection)
    4. AddStreams:A客户端添加本地音视频流到PeerConnection
    5. CreateOffer:A客户端创建Offer并发送给信令服务器,由信令服务器转给B客户端。Offer中包含本地的网络信息(SDP)。
    6. CreateAnswer:B客户端收到Offer后,创建放到自己的PeerConnection中,并获取自己的网络信息(SDP),通过发送给信令服务器,由信令服务器转发给A客户端。
    7. 上面步骤进行完毕后,开始通过信令服务器(coturn),双方客户端获取自己的地址作为候选人“candidate”,然后通过websocket发送给对方。彼此拿到候选地址后,互相进行访问测试,建立链接。
    8. OnAddStream:获取对方音视频流,PeerConnection有ontrack监听器能拿到对方视频流数据。

2. 搭建WebSocket服务器

看例子中代码,使用nodejs启动

3. 搭建Coturn音视频穿透服务器

公司内网虚拟机中穿透服务器Coturn的搭建

4. 遇到的问题

后面再慢慢补吧,问题有点多

5. 例子

  1. 客户端代码使用html+js编写
  2. WebSocket代码使用js编写使用nodejs运行
  3. android端代码请下载:WebRtcAndroidDemo

5.1 客户端代码

  1. 引入adapter-latest.js文件,此文件如果过期了就自己百度找找吧。
  2. 将ws://192.168.1.60:9001/ws改为自己websocket服务所在电脑的ip地址,在本地启动则是本机地址
  3. 将iceServers中的ip改为coturn服务器所在ip地址
<html>
    <head>
        <title>Voice WebRTC demo</title>
    </head>
    <h1>WebRTC demo 1v1</h1>

    <div id="buttons">
        <input id="zero-roomId" type="text" placeholder="请输入房间ID" maxlength="40"/>
        <button id="joinBtn" type="button">加入</button>
        <button id="leaveBtn" type="button">离开</button>    
    </div>
    <div id="videos">
        <video id="localVideo" autoplay muted playsinline>本地窗口</video>
        <video id="remoteVideo" autoplay playsinline>远端窗口</video>
    </div>
    <script src="js/main.js"></script>
    <!-- 可直接引入在线js:https://webrtc.github.io/adapter/adapter-latest.js  -->
    <script src="js/adapter-latest.js"></script>
</html>
'use strict';

// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";


var localUserId = Math.random().toString(36).substr(2); // 本地uid
var remoteUserId = -1;      // 对端
var roomId = 0;

var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var localStream = null;
var remoteStream = null;
var pc = null;

var zeroRTCEngine;


function handleIceCandidate(event) {
    console.info("handleIceCandidate");
    if (event.candidate) {
        var candidateJson = {
            'label': event.candidate.sdpMLineIndex,
            'id': event.candidate.sdpMid,
            'candidate': event.candidate.candidate
        };
        var jsonMsg = {
            'cmd': SIGNAL_TYPE_CANDIDATE,
            'roomId': roomId,
            'uid': localUserId,
            'remoteUid':remoteUserId,
            'msg': JSON.stringify(candidateJson) 
        };
        var message = JSON.stringify(jsonMsg);
        zeroRTCEngine.sendMessage(message);
        console.info("handleIceCandidate message: " + message);
        console.info("send candidate message");
    } else {
        console.warn("End of candidates");
    }
}

function handleRemoteStreamAdd(event) {
    console.info("handleRemoteStreamAdd");
    remoteStream = event.streams[0];
    // 视频轨道
    // let videoTracks = remoteStream.getVideoTracks()
    // 音频轨道
    // let audioTracks = remoteStream.getAudioTracks()
    remoteVideo.srcObject = remoteStream;
}

function handleConnectionStateChange() {
    if(pc != null) {
        console.info("ConnectionState -> " + pc.connectionState);
    }
}

function handleIceConnectionStateChange() {
    if(pc != null) {
        console.info("IceConnectionState -> " + pc.iceConnectionState);
    }
}


function createPeerConnection() {
    var defaultConfiguration = {  
        bundlePolicy: "max-bundle",
        rtcpMuxPolicy: "require",
        iceTransportPolicy:"all",//relay 或者 all
        // 修改ice数组测试效果,需要进行封装
        iceServers: [
            {
                "urls": [
                    "turn:192.168.1.173:3478?transport=udp",
                    "turn:192.168.1.173:3478?transport=tcp"       // 可以插入多个进行备选
                ],
                "username": "lqf",
                "credential": "123456"
            },
            {
                "urls": [
                    "stun:192.168.1.173:3478"
                ]
            }
        ]
    };

    pc = new RTCPeerConnection(defaultConfiguration); // 音视频通话的核心类
    pc.onicecandidate = handleIceCandidate;
    pc.ontrack = handleRemoteStreamAdd;
    pc.onconnectionstatechange = handleConnectionStateChange;
    pc.oniceconnectionstatechange = handleIceConnectionStateChange

    localStream.getTracks().forEach((track) => pc.addTrack(track, localStream)); // 把本地流设置给RTCPeerConnection
}

function createOfferAndSendMessage(session) {
    pc.setLocalDescription(session)
        .then(function () {
            var jsonMsg = {
                'cmd': 'offer',
                'roomId': roomId,
                'uid': localUserId,
                'remoteUid': remoteUserId,
                'msg': JSON.stringify(session)
            };
            var message = JSON.stringify(jsonMsg);
            zeroRTCEngine.sendMessage(message);
            // console.info("send offer message: " + message);
            console.info("send offer message");
        })
        .catch(function (error) {
            console.error("offer setLocalDescription failed: " + error);
        });

}

function handleCreateOfferError(error) {
    console.error("handleCreateOfferError: " + error);
}

function createAnswerAndSendMessage(session) {
    pc.setLocalDescription(session)
        .then(function () {
            var jsonMsg = {
                'cmd': 'answer',
                'roomId': roomId,
                'uid': localUserId,
                'remoteUid': remoteUserId,
                'msg': JSON.stringify(session)
            };
            var message = JSON.stringify(jsonMsg);
            zeroRTCEngine.sendMessage(message);
            // console.info("send answer message: " + message);
            console.info("send answer message");
        })
        .catch(function (error) {
            console.error("answer setLocalDescription failed: " + error);
        });

}

function handleCreateAnswerError(error) {
    console.error("handleCreateAnswerError: " + error);
}



var ZeroRTCEngine = function (wsUrl) {
    this.init(wsUrl);
    zeroRTCEngine = this;
    return this;
}

ZeroRTCEngine.prototype.init = function (wsUrl) {
    // 设置websocket  url
    this.wsUrl = wsUrl;
    /** websocket对象 */
    this.signaling = null;
}

ZeroRTCEngine.prototype.createWebsocket = function () {
    zeroRTCEngine = this;
    zeroRTCEngine.signaling = new WebSocket(this.wsUrl);

    zeroRTCEngine.signaling.onopen = function () {
        zeroRTCEngine.onOpen();
    }

    zeroRTCEngine.signaling.onmessage = function (ev) {
        zeroRTCEngine.onMessage(ev);
    }

    zeroRTCEngine.signaling.onerror = function (ev) {
        zeroRTCEngine.onError(ev);
    }

    zeroRTCEngine.signaling.onclose = function (ev) {
        zeroRTCEngine.onClose(ev);
    }
}

ZeroRTCEngine.prototype.onOpen = function () {
    console.log("websocket打开");
}
ZeroRTCEngine.prototype.onMessage = function (event) {
    console.log("websocket收到信息: " + event.data);
    var jsonMsg = null;
    try {
         jsonMsg = JSON.parse(event.data);
    } catch(e) {
        console.warn("onMessage parse Json failed:" + e);
        return;
    }
    switch (jsonMsg.cmd) {
        case SIGNAL_TYPE_NEW_PEER:
            handleRemoteNewPeer(jsonMsg);
            break;
        case SIGNAL_TYPE_RESP_JOIN:
            handleResponseJoin(jsonMsg);
            break;
        case SIGNAL_TYPE_PEER_LEAVE:
            handleRemotePeerLeave(jsonMsg);
            break;
        case SIGNAL_TYPE_OFFER:
            handleRemoteOffer(jsonMsg);
            break;
        case SIGNAL_TYPE_ANSWER:
            handleRemoteAnswer(jsonMsg);
            break;
        case SIGNAL_TYPE_CANDIDATE:
            handleRemoteCandidate(jsonMsg);
            break;
    }
}

ZeroRTCEngine.prototype.onError = function (event) {
    console.log("onError: " + event.data);
}

ZeroRTCEngine.prototype.onClose = function (event) {
    console.log("onClose -> code: " + event.code + ", reason:" + EventTarget.reason);
}

ZeroRTCEngine.prototype.sendMessage = function (message) {
    this.signaling.send(message);
}

function handleResponseJoin(message) {
    console.info("handleResponseJoin, remoteUid: " + message.remoteUid);
    remoteUserId = message.remoteUid;
    // doOffer();
}

function handleRemotePeerLeave(message) {
    console.info("handleRemotePeerLeave, remoteUid: " + message.remoteUid);
    remoteVideo.srcObject = null;
    if(pc != null) {
        pc.close();
        pc = null;
    }
}

function handleRemoteNewPeer(message) {
    console.info("处理远端新加入链接,并发送offer, remoteUid: " + message.remoteUid);
    remoteUserId = message.remoteUid;
    doOffer();
}

function handleRemoteOffer(message) {
    console.info("handleRemoteOffer");
    if(pc == null) {
        createPeerConnection();
    }
    var desc = JSON.parse(message.msg);
    pc.setRemoteDescription(desc);
    doAnswer();
}

function handleRemoteAnswer(message) {
    console.info("handleRemoteAnswer");
    var desc = JSON.parse(message.msg);
    pc.setRemoteDescription(desc);
}

function handleRemoteCandidate(message) {
    console.info("handleRemoteCandidate");
    var jsonMsg = message.msg;
    if(typeof message.msg === "string"){
        jsonMsg = JSON.parse(message.msg);
    }
    var candidateMsg = {
        'sdpMLineIndex': jsonMsg.label,
        'sdpMid': jsonMsg.id,
        'candidate': jsonMsg.candidate
    };
    var candidate = new RTCIceCandidate(candidateMsg);
    pc.addIceCandidate(candidate).catch(e => {
        console.error("addIceCandidate failed:" + e.name);
    });
}

function doOffer() {
    // 创建RTCPeerConnection
    if (pc == null) {
        createPeerConnection();
    }
    // let options = {offerToReceiveVideo:true}
    // pc.createOffer(options).then(createOfferAndSendMessage).catch(handleCreateOfferError);
    pc.createOffer().then(createOfferAndSendMessage).catch(handleCreateOfferError);
}

function doAnswer() {
    pc.createAnswer().then(createAnswerAndSendMessage).catch(handleCreateAnswerError);
}


function doJoin(roomId) {
    var jsonMsg = {
        'cmd': 'join',
        'roomId': roomId,
        'uid': localUserId,
    };
    var message = JSON.stringify(jsonMsg);
    zeroRTCEngine.sendMessage(message);
    console.info("doJoin message: " + message);
}

function doLeave() {
    var jsonMsg = {
        'cmd': 'leave',
        'roomId': roomId,
        'uid': localUserId,
    };
    var message = JSON.stringify(jsonMsg);
    zeroRTCEngine.sendMessage(message);
    console.info("doLeave message: " + message);
    hangup();
}

function hangup() {
    localVideo.srcObject = null; // 0.关闭自己的本地显示
    remoteVideo.srcObject = null; // 1.不显示对方
    closeLocalStream(); // 2. 关闭本地流
    if(pc != null) {
        pc.close(); // 3.关闭RTCPeerConnection
        pc = null;
    }
}

function closeLocalStream() {
    if(localStream != null) {
        localStream.getTracks().forEach((track) => {
                track.stop();
        });
    }
}

function openLocalStream(stream) {
    console.log('Open local stream');
    doJoin(roomId);
    localVideo.srcObject = stream;      // 显示画面
    localStream = stream;   // 保存本地流的句柄
}


function initLocalStream() {
    navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true
    })
        .then(openLocalStream)
        .catch(function (e) {
            alert("getUserMedia() error: " + e.name);
        });
}
// zeroRTCEngine = new ZeroRTCEngine("wss://192.168.1.60:80/ws");
zeroRTCEngine = new ZeroRTCEngine("ws://192.168.1.60:9001/ws");
zeroRTCEngine.createWebsocket();

document.getElementById('joinBtn').onclick = function () {
    roomId = document.getElementById('zero-roomId').value;
    if (roomId == "" || roomId == "请输入房间ID") {
        alert("请输入房间ID");
        return;
    }
    console.log("第一步:加入按钮被点击, roomId: " + roomId);
    // 初始化本地码流
    initLocalStream();
}

document.getElementById('leaveBtn').onclick = function () {
    console.log("离开按钮被点击");
    doLeave();
}

5.2. 编写websocket服务

  1. 使用nodejs启动
var ws = require("nodejs-websocket")
var prort = 9001;

// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";

/** ----- ZeroRTCMap ----- */
var ZeroRTCMap = function () {
    this._entrys = new Array();

    this.put = function (key, value) {
        if (key == null || key == undefined) {
            return;
        }
        var index = this._getIndex(key);
        if (index == -1) {
            var entry = new Object();
            entry.key = key;
            entry.value = value;
            this._entrys[this._entrys.length] = entry;
        } else {
            this._entrys[index].value = value;
        }
    };
    this.get = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? this._entrys[index].value : null;
    };
    this.remove = function (key) {
        var index = this._getIndex(key);
        if (index != -1) {
            this._entrys.splice(index, 1);
        }
    };
    this.clear = function () {
        this._entrys.length = 0;
    };
    this.contains = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? true : false;
    };
    this.size = function () {
        return this._entrys.length;
    };
    this.getEntrys = function () {
        return this._entrys;
    };
    this._getIndex = function (key) {
        if (key == null || key == undefined) {
            return -1;
        }
        var _length = this._entrys.length;
        for (var i = 0; i < _length; i++) {
            var entry = this._entrys[i];
            if (entry == null || entry == undefined) {
                continue;
            }
            if (entry.key === key) {// equal
                return i;
            }
        }
        return -1;
    };
}

var roomTableMap = new ZeroRTCMap();

function Client(uid, conn, roomId) {
    this.uid = uid;     // 用户所属的id
    this.conn = conn;   // uid对应的websocket连接
    this.roomId = roomId;
}

function handleJoin(message, conn) {
    var roomId = message.roomId;
    var uid = message.uid;

    console.info("uid: " + uid + "try to join room " + roomId);

    var roomMap = roomTableMap.get(roomId);
    if (roomMap == null) {
        roomMap = new  ZeroRTCMap();        // 如果房间没有创建,则新创建一个房间
        roomTableMap.put(roomId, roomMap);
    }

    if(roomMap.size() >= 2) {
        console.error("roomId:" + roomId + " 已经有两人存在,请使用其他房间");
        // 加信令通知客户端,房间已满
        return null;
    }

    var client = new Client(uid, conn, roomId);
    roomMap.put(uid, client);
    if(roomMap.size() > 1) {
        // 房间里面已经有人了,加上新进来的人,那就是>=2了,所以要通知对方
        var clients = roomMap.getEntrys();
        for(var i in clients) {
            var remoteUid = clients[i].key;
            if (remoteUid != uid) {
                var jsonMsg = {
                    'cmd': SIGNAL_TYPE_NEW_PEER,
                    'remoteUid': uid
                };
                var msg = JSON.stringify(jsonMsg);
                var remoteClient =roomMap.get(remoteUid);
                console.info("new-peer: " + msg);
                remoteClient.conn.sendText(msg);

                jsonMsg = {
                    'cmd':SIGNAL_TYPE_RESP_JOIN,
                    'remoteUid': remoteUid
                };
                msg = JSON.stringify(jsonMsg);
                console.info("resp-join: " + msg);
                conn.sendText(msg);
            }
        }
    }

    return client;
}

function handleLeave(message) {
    var roomId = message.roomId;
    var uid = message.uid;

    var roomMap = roomTableMap.get(roomId);
    if (roomMap == null) {
        console.error("handleLeave can't find then roomId " + roomId);
        return;
    }
    if (!roomMap.contains(uid)) {
        console.info("uid: " + uid +" have leave roomId " + roomId);
        return;
    }
    
    console.info("uid: " + uid + " leave room " + roomId);
    roomMap.remove(uid);        // 删除发送者
    if(roomMap.size() >= 1) {
        var clients = roomMap.getEntrys();
        for(var i in clients) {
            var jsonMsg = {
                'cmd': 'peer-leave',
                'remoteUid': uid // 谁离开就填写谁
            };
            var msg = JSON.stringify(jsonMsg);
            var remoteUid = clients[i].key;
            var remoteClient = roomMap.get(remoteUid);
            if(remoteClient) {
                console.info("notify peer:" + remoteClient.uid + ", uid:" + uid + " leave");
                remoteClient.conn.sendText(msg);
            }
        }
    }
}

function handleForceLeave(client) {
    var roomId = client.roomId;
    var uid = client.uid;

    // 1. 先查找房间号
    var roomMap = roomTableMap.get(roomId);
    if (roomMap == null) {
        console.warn("handleForceLeave can't find then roomId " + roomId);
        return;
    }

    // 2. 判别uid是否在房间
    if (!roomMap.contains(uid)) {
        console.info("uid: " + uid +" have leave roomId " + roomId);
        return;
    }

    // 3.走到这一步,说明客户端没有正常离开,所以我们要执行离开程序
    console.info("uid: " + uid + " force leave room " + roomId);

    roomMap.remove(uid);        // 删除发送者
    if(roomMap.size() >= 1) {
        var clients = roomMap.getEntrys();
        for(var i in clients) {
            var jsonMsg = {
                'cmd': 'peer-leave',
                'remoteUid': uid // 谁离开就填写谁
            };
            var msg = JSON.stringify(jsonMsg);
            var remoteUid = clients[i].key;
            var remoteClient = roomMap.get(remoteUid);
            if(remoteClient) {
                console.info("notify peer:" + remoteClient.uid + ", uid:" + uid + " leave");
                remoteClient.conn.sendText(msg);
            }
        }
    }
}

function handleOffer(message) {
    var roomId = message.roomId;
    var uid = message.uid;
    var remoteUid = message.remoteUid;

    console.info("handleOffer uid: " + uid + "transfer  offer  to remoteUid" + remoteUid);

    var roomMap = roomTableMap.get(roomId);
    if (roomMap == null) {
        console.error("handleOffer can't find then roomId " + roomId);
        return;
    }

    if(roomMap.get(uid) == null) {
        console.error("handleOffer can't find then uid " + uid);
        return;
    }

    var remoteClient = roomMap.get(remoteUid);
    if(remoteClient) {
        var msg = JSON.stringify(message);
        remoteClient.conn.sendText(msg);    //把数据发送给对方
    } else {
        console.error("can't find remoteUid: " + remoteUid);
    }
}

function handleAnswer(message) {
    var roomId = message.roomId;
    var uid = message.uid;
    var remoteUid = message.remoteUid;

    console.info("handleAnswer uid: " + uid + "transfer answer  to remoteUid" + remoteUid);

    var roomMap = roomTableMap.get(roomId);
    if (roomMap == null) {
        console.error("handleAnswer can't find then roomId " + roomId);
        return;
    }

    if(roomMap.get(uid) == null) {
        console.error("handleAnswer can't find then uid " + uid);
        return;
    }

    var remoteClient = roomMap.get(remoteUid);
    if(remoteClient) {
        var msg = JSON.stringify(message);
        remoteClient.conn.sendText(msg);
    } else {
        console.error("can't find remoteUid: " + remoteUid);
    }
}

function handleCandidate(message) {
    var roomId = message.roomId;
    var uid = message.uid;
    var remoteUid = message.remoteUid;

    console.info("处理Candidate uid: " + uid + "transfer candidate  to remoteUid" + remoteUid);

    var roomMap = roomTableMap.get(roomId);
    if (roomMap == null) {
        console.error("handleCandidate can't find then roomId " + roomId);
        return;
    }

    if(roomMap.get(uid) == null) {
        console.error("handleCandidate can't find then uid " + uid);
        return;
    }

    var remoteClient = roomMap.get(remoteUid);
    if(remoteClient) {
        var msg = JSON.stringify(message);
        remoteClient.conn.sendText(msg);
    } else {
        console.error("can't find remoteUid: " + remoteUid);
    }
}
// 创建监听9001端口webSocket服务
var server = ws.createServer(function(conn){
    console.log("创建一个新的连接--------")
    conn.client = null; // 对应的客户端信息
    // conn.sendText("我收到你的连接了....");
    conn.on("text", function(str) {
        // console.info("recv msg:" + str);
        var jsonMsg = JSON.parse(str);

        switch (jsonMsg.cmd) {
            case SIGNAL_TYPE_JOIN:
                conn.client = handleJoin(jsonMsg, conn);
                break;
            case SIGNAL_TYPE_LEAVE:
                handleLeave(jsonMsg);
                break;
            case SIGNAL_TYPE_OFFER:
                handleOffer(jsonMsg);
                break;   
            case SIGNAL_TYPE_ANSWER:
                handleAnswer(jsonMsg);
                break; 
            case SIGNAL_TYPE_CANDIDATE:
                handleCandidate(jsonMsg);
                break;      
        }

    });

    conn.on("close", function(code, reason) {
        console.info("连接关闭 code: " + code + ", reason: " + reason);
        if(conn.client != null) {
            // 强制让客户端从房间退出
            handleForceLeave(conn.client);
        }
    });

    conn.on("error", function(err) {
        console.info("监听到错误:" + err);
    });
}).listen(prort);

6. 参考文档

  1. WebRtc接口参考
  2. WebRTC 传输协议详解
  3. WebRTC的学习(java版本信令服务)
  4. Android webrtc实战(一)录制本地视频并播放,附带详细的基础知识讲解
  5. webSocket(wss)出现连接失败的问题解决方法
  6. 最重要的是这个,完整听了课程:2023最新Webrtc基础教程合集,涵盖所有核心内容(Nodejs+vscode+coturn
WebRTC音视频开发 自研实时音视频(RTC)系统的架构在客户端、服务端、运维、测试和质量监控上都存在很多难点一个实时音视频应用共包括几个环节:采集、编码、前后处理、传输、解码、渲染等很多环节。每一个环节又有更细分的技术模块,比如前后处理环节有美颜、滤镜、回声消除、噪声抑制等,编解码有VP8、VP9、H.264、H.265等。 阅读详情

相关推荐

手把手教你使用WebRTC

其实我们WebSocket做的事情在WebRTC中被叫做信令服务器,信令就是双方发送的所有信息,你可以通过任何形式传输这些信息,WebSocket也好Http请求也可以,因为信令服务器不需要理解这些中间信息,也不需要做额外的处理,唯一要做的就是把信息带到另外一方。二、交换ICEWebRTC使用了ICE(交互式连接设施)的框架协议,在建立连接之前两个客户端需要互相交换,简单的说就是交换网络信息,告知两个客户端互相的地址。这里双方做的事就是存下自己的信息,然后发送和接收对方的Offer或Answer。...

yinshipin007的博客 5661

WebRTC入门

通过一个github项目学习搭建一个简单可用的webrtc点对点投屏/视频通话实现

weixin_47794762的博客 2400

WebRTC之实现1v1音视频通话

title: WebRTC之实现1v1音视频通话 date: 2019-12-05 21:06:38 tags: - WebRTC - 音视频 categories: - WebRTC 原文: https://ahoj.cc/ 打通一下 1v1 音视频通话的流程。 基本知识 端到端连接基本流程: 客户端信令消息设计: join 加入房间 leave 离开房间 message 端到端消息 ...

ahojcn 的博客 2994

基于WebRTC实现音视频通话

随着互联网的发展,实时音视频通话功能已经成为远程办公、社交娱乐和在线教育等领域中不可或缺的一项重要功能。WebRTC作为一种开放标准的实时通信协议,能轻松实现浏览器之间的实时音视频通信。本次主要分享基于WebRTC的音视频通话技术,讲解WebRTC原理和音视频传输等关键概念,通过案例实践,带大家掌握如何搭建一个音视频通话应用。

黑马程序员官方博客 6389

WebRTC实现音视频通话全流程

WebRTC技术实现浏览器端实时音视频通信 摘要:WebRTC是一项无需插件即可实现浏览器间点对点音视频传输的技术,广泛应用于视频会议、在线教育等场景。本文详细介绍了WebRTC的实现过程:1)通过信令服务器建立连接;2)交换SDP和网络信息;3)创建RTCPeerConnection对象;4)通过事件监听实现媒体流传输。具体实现包括:使用Socket.io作为信令服务器,客户端通过加入房间、发起/接收呼叫、交换offer/answer和candidate信息等步骤建立连接。文中还提供了完整的Vue+Typ

lovehu6686的博客 1089

WebRTC 实现P2P音视频通话——搭建信令服务器

WebRTC 实现P2P音视频通话——搭建信令服务器

FF_lz的博客 7837

WebRtc实现1V1音视频通话

WebRTC,名称源自网页实时通信(Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音通话或视频聊天的技术,是谷歌 2010 年以 6820 万美元收购 Global IP Solutions 公司而获得的一项技术。WebRTC 提供了实时音视频的核心技术,包括音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:windows,linux,mac,android。官方文档官方中文文档相关中文文档。

weixin_48974246的博客 4038

5G时代必学的WebRTC音视频通话技术

什么是WebRTC ◼ WebRTC(Web Real-Time Communication)是 Google于2010以6829万美 元从 Global IP Solutions 公司购买,并于2011年将其开源,旨在建立一个互联 网浏览器间的实时通信的平台,让 WebRTC技术成为 H5标准之一。 WebRTC时代来临 所有主要的浏览器的兼容 WebRTC现在得到了所有主要浏览器的支持和采用,...

Linuxhus的博客 1887

18 基于WebRTC实现音视频通话

经过上述三个步骤,则完成了 P2P 通信过程中的媒体协商部分,实际上在呼叫端以及接收端调用setLocalDesccription 同时也开始了收集各端自己的网络信息(candidate),然后各端通过监听事件 onicecandidate 收集到各自的 candidate 并通过信令服务器传送给对端,进而打通 P2P 通信的网络通道,并通过监听 onaddstream 事件拿到对方的视频流进而完成了整个视频通话过程。因此:参与音视频通讯的双方想要了解对方支持的媒体格式,必须要交换 SDP 信息。

THE ORDER 2098

IOS端使用WebRTC实现一对一音视频通话

前言WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话将基于前两篇博客的基础环境之上实现IOS原生客户端P2P音视频通话。

ForeverMyheart的博客 3070

使用Power Apps实现SharePoint Online列表的下拉菜单的级联选择

在SharePoint列表中添加数据的时候,下拉菜单的级联选择一直是一个非常实用的功能,尤其是有很多数据的情况,级联选择可以根据上一层的选择,自动过滤下一层的选择,方便用户查找和选择准确的信息。一个最常用的情景是选择供应商的地区和国家,例如有三个列表,一个是供应商所在的大洲,一个是国家,一个是供应商。当用户选择州的时候,会自动列出属于这个州的国家。避免用户迷失在大量的数据中。 这里展示一下如何在...

shrenk的专栏 7808

WebRTC——从0至1打造实时音视频通话项目(Demo)

本文使用WebRTC技术,从0到1打造了实时音视频通话功能的Demo(附源码)

HuoZhiyan 的博客 2340

汽车电子开发必看:AUTOSAR MCAL层SPI驱动配置全解析(基于EB tresos)

本文深入解析了AUTOSAR MCAL层SPI驱动的核心配置,基于EB tresos工具详细阐述了通道、外部设备、作业与序列等关键概念。重点剖析了多设备共享总线时的资源管理与实时性调优策略,特别是SpiJobPriority参数的决定性影响,并提供了常见配置陷阱的实战排查指南,旨在帮助汽车电子工程师高效完成SPI驱动配置。

weixin_29188043的博客 290

webrtc音视频通话(二)简单音视频通话

webrtc音视频通话(二)简单音视频通话 这里不详细介绍websocket,只针对websocket整合rtc。如果不会websokcet的,可以转到 springboot整合websocket(一)简单聊天室 一、简单说下webrtc的流程 webrtc是P2P通信,也就是实际交流的只有两个人,而要建立通信,这两个人需要交换一些信息来保证通信安全。而且,webrtc必须通过ssh加密,也就是使用https协议、wss协议。 借用一幅图 1.1 创建端点的解析 以下解析不包括websockt,只

qq_44938451的博客 9443

视频倍速 - 在线调倍速的加速器APP工具

一刀工具箱APP提供一键可将手机上的视频倍速播放,可以调整视频倍速的加速器,调节视频的速度也就是调节视频的播放速度!一键可将手机上的视频倍速播放,可以调整视频倍速的加速器,调节视频的速度也就是调节视频的播放速度!视频倍速-在线调倍速的加速器-一刀工具箱APP。...

无涯教程 7551

最小二乘法(看完即会)

其中,X是数据矩阵,每一行是一个数据点,每一列是一个自变量(包括截距项),Y是观测值向量,β是待求的系数向量。为了找到使S最小的a和b的值,我们需要对S求关于a和b的偏导数,并令它们等于零。最小二乘法的数学公式通常用于线性回归问题,其中目标是找到一条直线(或更高维度的超平面),使得这条直线与给定数据点的误差平方和最小。同样地,通过求偏导数和令偏导数等于零,我们可以得到一个线性方程组,解这个方程组可以找到最优的b_0, b_1, …+ b_m*x_m,其中b_0是截距,b_1到b_m是各个自变量的系数。

qlkaicx的博客 2万+

WebRTC实现简单音视频通话能力

本文介绍如何基于WebRTC快速实现一个简单的实时音视频通话。流:一组按指定编码格式封装的音视频数据内容。一个流可以包含几个轨道,比如视频和音频轨道。推流:把采集阶段封包好的音视频数据流推送到 ZEGO 实时音视频云的过程。拉流:从 ZEGO 实时音视频云将已有音视频数据流拉取播放的过程。房间:是 ZEGO 提供的音视频空间服务,用于组织用户群,同一房间内的用户可以互相收发实时音视频及消息。用户需要先登录某个房间,才能进行音视频推流、拉流操作。

ZEGO即构科技 1680

手把手教你用MOS管设计双电源自动切换电路(附零压降方案)

本文详细介绍了使用MOS管设计双电源自动切换电路的完整方案,包括基础电路拓扑、零压降优化设计及实战调试技巧。针对外接电源与电池供电的无缝切换需求,提供低损耗MOS管选型建议和性能实测数据,帮助工程师解决传统二极管方案的压降与漏电问题。

h0i1j2k3l的博客 1437

Android端基于WebRTC的音视频通话小demo

版本信息 AndroidStudio :3.5.2 org.webrtc:google-webrtc:1.0.28513 WebRTC简介 WebRTC是什么 WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API。它于2011年6月1日开源并在Google、Mozilla、Opera支持...

吓成一坨兔子 7482

使用IDF和VSCode构建ESP32的开发环境

本文主要介绍构建使用IDF和VS-CODE 工具构建ESP32的开发环境的详细步骤,还应用IDF和VS CODE创建ESP32的项目文件,并实现编译和下载功能。

mftang的博客 6818

PCL 直通滤波

内容抄自优快云点云侠:[【2024最新版】PCL点云处理算法汇总(C++长期更新版)](https://blog.youkuaiyun.com/qq_36686437/article/details/114160640)。质量无忧,永久免费,可放心复制粘贴。

m0_51204289的博客 902
上一篇: ffmpeg合成mp3音频,解决音频属性不一致问题
just_you_java
博客等级 码龄12年 1689粉丝 85原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值