【音视频】WebRTC 一对一通话-Web端

发布于:2025-08-06 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、前端页面

前端页面的设计如下,有一个房间输入文本框,一个加入房间按钮和离开房间按钮,然后下面有两个视频窗口,分别是显示本地的窗口和远端的窗口

在这里插入图片描述

对应的html代码也很简单,就是需要注意将本地视频的属性设置上静音,否则有回声,使用两个<div>标签分别包裹本地视频和远端视频,完整的html代码如下:

<!DOCTYPE html>

<html>

<head>
    <title>WebRTC一对一通话</title>
</head>

<body>
    <h1>WebRTC一对一通话</h1>
    <div id="buttonsDiv">
        <input id="roomIdInput" type="text" placeholder="请输入房间ID">
        <button id="joinBtn">加入房间</button>
        <button id="leaveBtn">离开房间</button>
    </div>
    <div id="localVideosDiv">
        <video id="localVideo" autoplay muted playsinline>本地窗口</video>
    </div>
    <div id="remoteVideoDiv">
        <video id="remoteVideo" autoplay playsinline>远端窗口</video>
    </div>
</body>

</html>

二、RTCEngine 类

我们使用WebSocket的方式与服务器保持长连接,我们下面实现一个RTCEngine类,这个类用于与服务器的连接,并且发送信令

2.1 构造函数

该函数的的构造函数需要传入服务器的url,然后使用这个url初始化本地的WebSocket连接:

var rtcEngine = null;
var RTCEngine = function (wsUrl) {
    //传入的函数形参
    this.init(wsUrl); //init相当于成员函数
    rtcEngine = this;
    return this; //返回自身实例
};

2.2 init 函数

  • 构造函数内部使用了init成员函数,我们可以使用prototype的方式来定义这个成员函数
  • init函数内部实现的就是对应的赋值操作,signaling是我们定义类的WebSocket成员对象
//初始化RTCEngine
RTCEngine.prototype.init = function (wsUrl) {
    this.wsUrl = wsUrl; //wsUrl是成员变量
    this.signaling = null; //websocket对象
};

2.3 createWebSocket 函数

内层封装

  • 我们在类内添加一个连接函数createWebSocket,用于创建一个WebSocket对象,并且根据构造函数得到的url向服务器发送连接请求:

  • 在函数内部需要实现对应的几个WebSocket回调函数,比如onopenonmessage等等

//创建WebSocket连接
RTCEngine.prototype.createWebSocket = function () {
    rtcEngine = this;
    rtcEngine.signaling = new WebSocket(this.wsUrl);

    //绑定WebSocket回调函数
    rtcEngine.signaling.onopen = function () {
        rtcEngine.onOpen();
    };
    rtcEngine.signaling.onmessage = function (event) {
        rtcEngine.onMessage(event);
    };
    rtcEngine.signaling.onerror = function (event) {
        rtcEngine.onError(event);
    };
    rtcEngine.signaling.onclose = function (event) {
        rtcEngine.onClose(event);
    };
};

外层封装

类内的回调函数调用的都是我们的成员函数,比如onOpenonMessage,这些我们需要在外部定义:

//WebSocket连接成功回调函数,RTCEngine的成员函数
RTCEngine.prototype.onOpen = function () {
    console.log("WebSocket连接成功");
};

//WebSocket连接失败回调函数,RTCEngine的成员函数
RTCEngine.prototype.onError = function (event) {
    console.log("WebSocket连接失败:" + event.data);
};

//WebSocket连接关闭回调函数,RTCEngine的成员函数
RTCEngine.prototype.onClose = function (event) {
    console.log("WebSocket连接关闭:" + event.data);
};

//WebSocket接收消息回调函数,RTCEngine的成员函数
RTCEngine.prototype.onMessage = function (event) {
    console.log("WebSocket接收消息:" + event.data);
};

2.4 sendMessageh函数

这个函数使用我们的sigaling对象发送消息:

//向WebSocket服务器发送消息
RTCEngine.prototype.sendMessage = function (message) {
    rtcEngine.signaling.send(message);
};

三、信令处理

3.1 信令定义

我们需要实现下面的信令,以实现本地端和远端的交互,期间通过服务器转发信令,我们的信令都是使用JSON格式序列化的:

  1. join 加入房间
  2. resp_­join 当join房间后发现房间已经存在另一个人时则返回另一个人的uid;如果只有自己则不返回
  3. leave 离开房间,服务器收到leave信令则检查同一房间是否有其他人,如果有其他人则通知他有人离开
  4. new_­peer 服务器通知客户端有新人加入,收到new­peer则发起连接请求
  5. peer_­leave 服务器通知客户端有人离开
  6. offer 转发offer sdp
  7. answer 转发answer sdp
  8. candidate 转发candidate sdp

信令的定义如下:

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"; //发送offer
const SIGNAL_TYPE_ANSWER = "answer"; //对端回复
const SIGNAL_TYPE_CANDIDATE = "candidate"; //发送candidate

3.2 信令响应

onMessage函数我们需要处理来自服务端的消息,解析后可以的到信令的类型,根据不同的类型我们做不同的处理:


//WebSocket接收消息回调函数,RTCEngine的成员函数
RTCEngine.prototype.onMessage = function (event) {
    console.log("WebSocket接收消息:" + event.data);

    //解析消息:JSON格式
    var msg = JSON.parse(event.data);
    console.log("解析后的JSON消息:", msg);
    switch (msg.cmd) {
        case SIGNAL_TYPE_NEW_PEER: //新加入者
            handleRemoteNewPeer(msg);
            break;
        case SIGNAL_TYPE_RESP_JOIN: //加入者回复
            handleResponseJoin(msg);
            break;
        case SIGNAL_TYPE_PEER_LEAVE: //离开者
            handleRemotePeerLeave(msg);
            break;
        case SIGNAL_TYPE_OFFER: //收到offer
            handleRemoteOffer(msg);
            break;
        case SIGNAL_TYPE_ANSWER: //收到answer
            handleRemoteAnswer(msg);
            break;
        case SIGNAL_TYPE_CANDIDATE: //收到candidate
            handleRemoteCandidate(msg);
            break;
    }
};

3.3 join

点击加入按钮的时候,首先需要查看按钮的信息是否合法,然后再进行后续的操作:

document.querySelector("#joinBtn").onclick = function () {

    if (isInRoom) {
        alert("请先离开当前房间");
        return;
    }
    roomId = document.getElementById("roomIdInput").value;
    if (roomId == "" || roomId == "请输入房间ID") {
        alert("请输入房间ID");
        return;
    }

    console.log("触发加入按钮点击事件,roomId = " + roomId);
    //初始化本地流
    initLocalStream();
};

信息合法,那么我们就可以打开本地摄像头,将摄像头画面添加到<video>标签内,对应的initLocalStream就是实现这样的功能,这里的可以选择麦克风和摄像头:

//初始化本地流
function initLocalStream() {
    navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true
    })
    .then(openLocalStream)
    .catch(function (e) {
        alert("getUserMedia() error: " + e.name);
    });
}

如果正常执行,那么就会异步执行openLocalStream,如果过程中发生了异常就执行捕捉异常的函数

var localVideo = document.querySelector("#localVideo");
var remoteVideo = document.querySelector("#remoteVideo");
//打开本地流并设置
function openLocalStream(stream) {
    console.log("打开本地流成功");

    //加入房间
    doJoin(roomId);

    //设置本地视频
    localVideo.srcObject = stream;
    localStream = stream;
}

然后我们开始封装join信令,使用rtcEngine这个对象内部的sendMessage函数发送,告诉服务器我们的uid,以及需要加入的房间号roomId

//加入房间,向服务器发送JSON消息
function doJoin(roomId) {
    var jsonMsg = {
        cmd: SIGNAL_TYPE_JOIN,
        roomId: roomId,
        uid: localUserId,
    };

    //发送JSON消息
    var message = JSON.stringify(jsonMsg);
    rtcEngine.sendMessage(message);

    console.log("发送JSON消息:" + message);
    isInRoom = true;
}

注意这里的uid实际只是我们随机生成的,实际需要修改这部分逻辑:

var localUserId = Math.random().toString(36).substring(2); //生成本地id

3.4 resp_join

这个信令触发代表我们当前加入了这个房间,并且服务器告诉我们房间里面的人是谁,返回的是远端的uid,这里我们需要做的就是将这个uid存储起来:

var remoteUserId = -1;
//有人加入,发送给加入者
function handleResponseJoin(msg) {
    console.log("加入者:" + msg.remoteUid);
    remoteUserId = msg.remoteUid;
}

3.5 new_peer

这个信令是当我们处在房间中,另一个人加入的时候,服务器告诉我们有人加入房间了,并且告诉我们这个人的uid,我们也是需要将它存储起来:

//有人加入,发送给房间的人
function handleRemoteNewPeer(msg) {
    console.log("房间里面的人:" + msg.remoteUid);
    remoteUserId = msg.remoteUid;
    doOffer();
}

当然,这里还需要就是给他发offer,也就是房间里面的人齐了,我们可以开始媒体协商了:

//加入房间,给房间里面的人发送offer
function doOffer() {
    //创建RTCPeerConnection对象
    if (pc == null) {
        createPeerConnection();
    }
    pc.createOffer().then(createOfferAndSendMessage).catch(function (error) {
        console.log("创建offer失败");
    });
}
  • 因此我们就需要使用到WebRTC的接口了,首先我们需要先创建一个WebRTCRTCPeerConnection对象
  • 我们先不配置stunturn服务器,使用浏览器默认的配置,传入null到构造函数
  • 然后还需要绑定两个回调函数,分别是当网络协商ICECandidate收集完成的时候触发、远端流到达本地时触发
  • 最后,我们遍历自己本地的所有流,加入到RTCPeerConnection的类中,帮助媒体协商
//创建和远端的连接
function createPeerConnection() {
    //创建RTCPeerConnection对象
    pc = new RTCPeerConnection(null);
    // pc = new RTCPeerConnection(null);

    //设置回调函数
    pc.onicecandidate = handleIceCandidate; //设置IceCandidate回调函数
    pc.ontrack = handleRemoteStreamAdd; //设置远端流回调函数

    //遍历本地流,加入流中
    localStream.getTracks().forEach(function (track) {
        pc.addTrack(track, localStream);
    });

    console.log("创建RTCPeerConnection对象成功");
}
  • 使用createOffer函数创建好的offer之后,会调用then里面的函数,会传入一个session到该函数,表示本地的媒体信息sdp,我们需要将它存储在本地,然后发送一份到对端:

  • 存储本地使用的setLocalDescription接口,存储成功后我们调用then里面的函数发出去到服务器

//发送offer并且发送sdp对象session,创建sdp
function createOfferAndSendMessage(session) {
    //设置本地sdp对象
    pc.setLocalDescription(session)
        .then(function () {
            //发送offer
            var jsonMsg = {
                "cmd": SIGNAL_TYPE_OFFER,
                "roomId": roomId,
                "uid": localUserId,
                "remoteUid": remoteUserId,
                "message": JSON.stringify(session),
            };
            var message = JSON.stringify(jsonMsg);
            rtcEngine.sendMessage(message);
            console.log("发送JSON消息:" + message);
        })
        .catch(function (error) {
            console.log("设置本地sdp对象失败");
        });
}

接收到对端流的时候,我们需要将对端流存储起来,然后将其渲染在<video>标签内:

//设置ontrack回调函数,接收到对端流时触发
function handleRemoteStreamAdd(event) {
    console.log("接收对端流");
    //设置对端流
    remoteStream = event.streams[0];
    remoteVideo.srcObject = remoteStream;
}

3.6 candidate

  • 收集到媒体协商后,触发回调函数,我们这里要将它发送到服务器,然后转发给对端,用于网络协商
  • ICECandidate的信息存储在回调函数的eventcandidate成员里面,它通常是一个JSON类型,我们将其序列化为字符串,存储到我们的JSON消息中,然后发给服务器
//设置onicecandidate回调函数,收集到Candidate信息时触发
function handleIceCandidate(event) {
    console.log("handleIceCandidate");
    if (event.candidate) {
        //发送candidate
        var jsonMsg = {
            "cmd": SIGNAL_TYPE_CANDIDATE,
            "roomId": roomId,
            "uid": localUserId,
            "remoteUid": remoteUserId,
            "message": JSON.stringify(event.candidate),
        };
        var message = JSON.stringify(jsonMsg);
        rtcEngine.sendMessage(message);

        console.log("发送candidate:" + message);
    }
    else {
        console.warn("结束candidate");
    }
}

同样,对端接收到candidate信令的响应,需要将candidate信息存储在本地,这里调用的是addIceCandidate接口:

//处理candidate
function handleRemoteCandidate(msg) {
    console.log("收到candidate");
    var candidate = JSON.parse(msg.message);

    //添加IceCandidate
    pc.addIceCandidate(candidate)
        .then(function () {
            console.log("添加IceCandidate成功");
        })
        .catch(function (error) {
            console.log("添加IceCandidate失败:" + error);
        });
}

当两端都设置好candidate的时候,可以认为连接已经建立了,前提是candidate至少有一条有效

3.7 offer

  • 房间第二个人加入的时候,会给另一个人发offer,我们收到offer的时候会响应,offer中包含着对端的媒体信息sdp,我们需要查看本地媒体信息,然后进行媒体协商,将协商好的sdp存储在自己的本地,然后回复一个answer到对端,answer中包含协商好的sdp信息,以及自己的对端的uid

  • 存储sdp信息的时候,调用RTCPeerConnection对象的setRemoteDescription接口实现

//收到offer,创建PeerConnection对象,保存sdp,并且回复answer
function handleRemoteOffer(msg) {
    console.log("收到offer");
    if (pc == null) {
        createPeerConnection();
    }

    var desc = JSON.parse(msg.message);
    pc.setRemoteDescription(desc); //设置对端sdp

    doAnswer(); //回复answer
}

3.8 answer

回复answer调用RTCPeerConnection对象的createAnswer接口实现,创建好answer之后,使用then里面的函数异步发送到服务器中:

//加入房间,房间里面的人回复answer
function doAnswer() {
    pc.createAnswer().then(createAnswerAndSendMessage).catch(function (error) {
        console.log("创建answer失败");
    });
}

该回调函数中会传入一个session,是协商好的媒体信息,使用setLocalDescription接口存储sdp,然后在通过then存储之后发送协商好的媒体信息到对端

//发送answer并且发送sdp对象session,对比对端sdp后设置本地sdp
function createAnswerAndSendMessage(session) {
    pc.setLocalDescription(session)
        .then(function () {
            //发送answer
            var jsonMsg = {
                "cmd": SIGNAL_TYPE_ANSWER,
                "roomId": roomId,
                "uid": localUserId,
                "remoteUid": remoteUserId,
                "message": JSON.stringify(session),
            };
            var message = JSON.stringify(jsonMsg);
            rtcEngine.sendMessage(message);
            console.log("发送JSON消息:" + message);
        })
        .catch(function (error) {
            console.log("设置本地sdp对象失败");
        });
}

3.10 leave

点击离开按钮,我们需要处理状态是否合法:

//设置离开按钮点击事件
document.querySelector("#leaveBtn").onclick = function () {
    if (roomId == -1) {
        alert("请先加入房间");
        return;
    }

    console.log("触发离开按钮点击事件,roomId = " + roomId);

    if(rtcEngine != null){
        doLeave();
    }
}

如果状态合法,那么我们就可以处理离开事件了,doLeave函数内部向服务器发送leave信令,告诉服务器自己离开了,同样封装JSON

//离开房间,向服务器发送JSON消息
function doLeave() {
    isInRoom = false;
    console.log("is in Room " + isInRoom);
    var jsonMsg = {
        cmd: SIGNAL_TYPE_LEAVE,
        roomId: roomId,
        uid: localUserId,
    };

    var message = JSON.stringify(jsonMsg);
    rtcEngine.sendMessage(message);

    console.log("发送JSON消息:" + message);
    hangup();
}

离开房间后,就可以将本地视频和远端的视频都关掉了,也就是在hangup函数内部实现的,这里还需要关闭RTCPeerConnection对象,后续会介绍这个类

//挂断视频
function hangup() {
    //关闭本地流
    if (localStream != null) {
        localStream.getTracks().forEach(function (track) {
            track.stop();
        });
        localVideo.srcObject = null;
        localStream = null;
    }

    //关闭远端流
    if (remoteStream != null) {
        remoteStream.srcObject = null;
        remoteStream = null;
    }

    if (remoteVideo != null) {
        remoteVideo.srcObject = null;
    }

    if (pc != null) {
        pc.close(); //关闭RTCPeerConnection对象
        pc = null;
    }
}

3.11 peer_leave

当有人离开的时候,服务器会告知房间里面的另一个人,已经有人离开了,那么此时就可以把远端的窗口给关闭了,并且关闭RTCPeerConnection对象、清空远端的用户信息:

//有人离开,发送给房间的人
function handleRemotePeerLeave(msg) {
    console.log("离开者:" + msg.remoteUid);
    remoteVideo.srcObject = null;
    remoteUserId = -1;
    if (pc != null) {
        pc.close();
        pc = null;
    }
}

四、完整代码

完整的Web端一对一视频通话代码如下:

4.1 index.html

页面显示代码:

<!DOCTYPE html>

<html>

<head>
    <title>WebRTC一对一通话</title>
</head>

<body>
    <h1>WebRTC一对一通话</h1>
    <div id="buttonsDiv">
        <input id="roomIdInput" type="text" placeholder="请输入房间ID">
        <button id="joinBtn">加入房间</button>
        <button id="leaveBtn">离开房间</button>
    </div>
    <div id="localVideosDiv">
        <video id="localVideo" autoplay muted playsinline>本地窗口</video>
    </div>
    <div id="remoteVideoDiv">
        <video id="remoteVideo" autoplay playsinline>远端窗口</video>
    </div>
</body>

<script src="js/main.js"></script>
<script> src = "js/adapter-latest.js" </script>

</html>

4.2 main.js

完整的信令实现代码

"use strict";

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"; //发送offer
const SIGNAL_TYPE_ANSWER = "answer"; //对端回复
const SIGNAL_TYPE_CANDIDATE = "candidate"; //发送candidate

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

var rtcEngine = null;

var localUserId = Math.random().toString(36).substring(2); //生成本地id
var remoteUserId = -1; //对端id
var roomId = 0; //房间号

var pc = null; //RTCPeerConnection对象

var isInRoom = false;

//相当于构造函数
var RTCEngine = function (wsUrl) {
    //传入的函数形参
    this.init(wsUrl); //init相当于成员函数
    rtcEngine = this;
    return this; //返回自身实例
};

//初始化RTCEngine
RTCEngine.prototype.init = function (wsUrl) {
    this.wsUrl = wsUrl; //wsUrl是成员变量
    this.signaling = null; //websocket对象
};

//创建WebSocket连接
RTCEngine.prototype.createWebSocket = function () {
    rtcEngine = this;
    rtcEngine.signaling = new WebSocket(this.wsUrl);

    //绑定WebSocket回调函数
    rtcEngine.signaling.onopen = function () {
        rtcEngine.onOpen();
    };
    rtcEngine.signaling.onmessage = function (event) {
        rtcEngine.onMessage(event);
    };
    rtcEngine.signaling.onerror = function (event) {
        rtcEngine.onError(event);
    };
    rtcEngine.signaling.onclose = function (event) {
        rtcEngine.onClose(event);
    };
};

//向WebSocket服务器发送消息
RTCEngine.prototype.sendMessage = function (message) {
    rtcEngine.signaling.send(message);
};

//WebSocket连接成功回调函数,RTCEngine的成员函数
RTCEngine.prototype.onOpen = function () {
    console.log("WebSocket连接成功");
};

//WebSocket连接失败回调函数,RTCEngine的成员函数
RTCEngine.prototype.onError = function (event) {
    console.log("WebSocket连接失败:" + event.data);
};

//WebSocket连接关闭回调函数,RTCEngine的成员函数
RTCEngine.prototype.onClose = function (event) {
    console.log("WebSocket连接关闭:" + event.data);
    isInRoom = false;
};

//WebSocket接收消息回调函数,RTCEngine的成员函数
RTCEngine.prototype.onMessage = function (event) {
    console.log("WebSocket接收消息:" + event.data);

    //解析消息:JSON格式
    var msg = JSON.parse(event.data);
    console.log("解析后的JSON消息:", msg);
    switch (msg.cmd) {
        case SIGNAL_TYPE_NEW_PEER: //新加入者
            handleRemoteNewPeer(msg);
            break;
        case SIGNAL_TYPE_RESP_JOIN: //加入者回复
            handleResponseJoin(msg);
            break;
        case SIGNAL_TYPE_PEER_LEAVE: //离开者
            handleRemotePeerLeave(msg);
            break;
        case SIGNAL_TYPE_OFFER: //收到offer
            handleRemoteOffer(msg);
            break;
        case SIGNAL_TYPE_ANSWER: //收到answer
            handleRemoteAnswer(msg);
            break;
        case SIGNAL_TYPE_CANDIDATE: //收到candidate
            handleRemoteCandidate(msg);
            break;
    }
};

//有人加入,发送给房间的人
function handleRemoteNewPeer(msg) {
    console.log("房间里面的人:" + msg.remoteUid);
    remoteUserId = msg.remoteUid;
    doOffer();
}

//有人加入,发送给加入者
function handleResponseJoin(msg) {
    console.log("加入者:" + msg.remoteUid);
    remoteUserId = msg.remoteUid;
}

//有人离开,发送给房间的人
function handleRemotePeerLeave(msg) {
    console.log("离开者:" + msg.remoteUid);
    remoteVideo.srcObject = null;
    remoteUserId = -1;
    if (pc != null) {
        pc.close();
        pc = null;
    }
}

//收到offer,创建PeerConnection对象,保存sdp,并且回复answer
function handleRemoteOffer(msg) {
    console.log("收到offer");
    if (pc == null) {
        createPeerConnection();
    }

    var desc = JSON.parse(msg.message);
    pc.setRemoteDescription(desc); //设置对端sdp

    doAnswer(); //回复answer
}

//收到answer,保存sdp
function handleRemoteAnswer(msg) {
    console.log("收到answer");
    var desc = JSON.parse(msg.message);
    pc.setRemoteDescription(desc); //设置对端sdp
}

//处理candidate
function handleRemoteCandidate(msg) {
    console.log("收到candidate");
    var candidate = JSON.parse(msg.message);

    //添加IceCandidate
    pc.addIceCandidate(candidate)
        .then(function () {
            console.log("添加IceCandidate成功");
        })
        .catch(function (error) {
            console.log("添加IceCandidate失败:" + error);
        });


}

//设置onicecandidate回调函数,收集到Candidate信息时触发
function handleIceCandidate(event) {
    console.log("handleIceCandidate");
    if (event.candidate) {
        //发送candidate
        var jsonMsg = {
            "cmd": SIGNAL_TYPE_CANDIDATE,
            "roomId": roomId,
            "uid": localUserId,
            "remoteUid": remoteUserId,
            "message": JSON.stringify(event.candidate),
        };
        var message = JSON.stringify(jsonMsg);
        rtcEngine.sendMessage(message);

        console.log("发送candidate:" + message);
    }
    else {
        console.warn("结束candidate");
    }
}

//设置ontrack回调函数,接收到对端流时触发
function handleRemoteStreamAdd(event) {
    console.log("接收对端流");
    //设置对端流
    remoteStream = event.streams[0];
    remoteVideo.srcObject = remoteStream;
}

//加入房间,向服务器发送JSON消息
function doJoin(roomId) {
    var jsonMsg = {
        cmd: SIGNAL_TYPE_JOIN,
        roomId: roomId,
        uid: localUserId,
    };

    //发送JSON消息
    var message = JSON.stringify(jsonMsg);
    rtcEngine.sendMessage(message);

    console.log("发送JSON消息:" + message);
    isInRoom = true;
}

//离开房间,向服务器发送JSON消息
function doLeave() {
    isInRoom = false;
    console.log("is in Room " + isInRoom);
    var jsonMsg = {
        cmd: SIGNAL_TYPE_LEAVE,
        roomId: roomId,
        uid: localUserId,
    };

    var message = JSON.stringify(jsonMsg);
    rtcEngine.sendMessage(message);

    console.log("发送JSON消息:" + message);
    hangup();
}

//加入房间,给房间里面的人发送offer
function doOffer() {
    //创建RTCPeerConnection对象
    if (pc == null) {
        createPeerConnection();
    }
    pc.createOffer().then(createOfferAndSendMessage).catch(function (error) {
        console.log("创建offer失败");
    });
}

//加入房间,房间里面的人回复answer
function doAnswer() {
    pc.createAnswer().then(createAnswerAndSendMessage).catch(function (error) {
        console.log("创建answer失败");
    });
}

//创建和远端的连接
function createPeerConnection() {
    //stun服务器配置信息
    var defaultConfiguration = {
        bundlePolicy: "max-bundle",
        rtcpMuxPolicy: "require",
        iceTransportPolicy: "relay", 
        iceServers: [
            {
                "urls": [
                    "turn:192.168.10.251:3478?transport=udp",
                    "turn:192.168.10.251:3478?transport=tcp" // 可以插入多个进行备选
                ],
                "username": "lh",
                "credential": "123456"
            },
            {
                "urls": [
                    "stun:192.168.10.251:3478"
                ]
            }
        ]
    };


    //创建RTCPeerConnection对象
    pc = new RTCPeerConnection(defaultConfiguration);
    // pc = new RTCPeerConnection(null);

    //设置回调函数
    pc.onicecandidate = handleIceCandidate; //设置IceCandidate回调函数
    pc.ontrack = handleRemoteStreamAdd; //设置远端流回调函数

    //遍历本地流,加入流中
    localStream.getTracks().forEach(function (track) {
        pc.addTrack(track, localStream);
    });

    console.log("创建RTCPeerConnection对象成功");
}

//发送offer并且发送sdp对象session,创建sdp
function createOfferAndSendMessage(session) {
    //设置本地sdp对象
    pc.setLocalDescription(session)
        .then(function () {
            //发送offer
            var jsonMsg = {
                "cmd": SIGNAL_TYPE_OFFER,
                "roomId": roomId,
                "uid": localUserId,
                "remoteUid": remoteUserId,
                "message": JSON.stringify(session),
            };
            var message = JSON.stringify(jsonMsg);
            rtcEngine.sendMessage(message);
            console.log("发送JSON消息:" + message);
        })
        .catch(function (error) {
            console.log("设置本地sdp对象失败");
        });
}

//发送answer并且发送sdp对象session,对比对端sdp后设置本地sdp
function createAnswerAndSendMessage(session) {
    pc.setLocalDescription(session)
        .then(function () {
            //发送answer
            var jsonMsg = {
                "cmd": SIGNAL_TYPE_ANSWER,
                "roomId": roomId,
                "uid": localUserId,
                "remoteUid": remoteUserId,
                "message": JSON.stringify(session),
            };
            var message = JSON.stringify(jsonMsg);
            rtcEngine.sendMessage(message);
            console.log("发送JSON消息:" + message);
        })
        .catch(function (error) {
            console.log("设置本地sdp对象失败");
        });
}

//挂断视频
function hangup() {
    //关闭本地流
    if (localStream != null) {
        localStream.getTracks().forEach(function (track) {
            track.stop();
        });
        localVideo.srcObject = null;
        localStream = null;
    }

    //关闭远端流
    if (remoteStream != null) {
        remoteStream.srcObject = null;
        remoteStream = null;
    }

    if (remoteVideo != null) {
        remoteVideo.srcObject = null;
    }

    if (pc != null) {
        pc.close(); //关闭RTCPeerConnection对象
        pc = null;
    }
}

//打开本地流并设置
function openLocalStream(stream) {
    console.log("打开本地流成功");

    //加入房间
    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);
    });
}

/*相当于main函数开始*/

//设置加入按钮点击事件
document.querySelector("#joinBtn").onclick = function () {

    if (isInRoom) {
        alert("请先离开当前房间");
        return;
    }
    roomId = document.getElementById("roomIdInput").value;
    if (roomId == "" || roomId == "请输入房间ID") {
        alert("请输入房间ID");
        return;
    }

    console.log("触发加入按钮点击事件,roomId = " + roomId);
    //初始化本地流
    initLocalStream();
};

//设置离开按钮点击事件
document.querySelector("#leaveBtn").onclick = function () {
    if (roomId == -1) {
        alert("请先加入房间");
        return;
    }

    console.log("触发离开按钮点击事件,roomId = " + roomId);

    if(rtcEngine != null){
        doLeave();
    }
}


//创建WebSocket连接
// rtcEngine = new RTCEngine("ws://192.168.217.128:9002");
rtcEngine = new RTCEngine("ws://192.168.10.251:9002");
rtcEngine.createWebSocket();

4.3 adpater-latest.js

这个文件是 WebRTC 项目中的一个适配器文件,其主要作用是为不同浏览器提供统一的 WebRTC API 接口,解决不同浏览器对 WebRTC 标准实现的差异问题

代码链接:
https://link.csdn.net/?from_id=79704635&target=https%3A%2F%2Fgithub.com%2Fwebrtc%2Fadapter

更多资料:https://github.com/0voice


网站公告

今日签到

点亮在社区的每一天
去签到