先看页面代码,这里使用的webRTC原生的API。目前信令服务器使用的是websocket实现的,后续改成将socket.io。socket.io默认含有房间的概念。
源代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>video</title>
</head>
<body>
<h2 style="text-align: center;">播放页面</h2>
<h3 id="userId" style="text-align: center;"></h3>
<center>
<div>
<video id="localVideo" class="video" autoplay="autoplay"></video>
<video id="remoteVideo" class="video" height="500px" autoplay="autoplay"></video>
</div>
</center>
</br>
<div style="text-align: center;">
<button id="callBtn" onclick="requestConnect()">建立连接</button>
<button id="hangupBtn" onclick="hangupHandle()">断开连接</button>
</div>
</br>
<div style="text-align: center;">
对方id: <input id="toUserId">
</div>
</body>
</html>
<script src="./adapter-latest.js"></script>
<script>
const localVideo = document.querySelector('#localVideo');
const remoteVideo = document.querySelector('#remoteVideo');
const callBtn = document.getElementById('callBtn')
const hangupBtn = document.getElementById('hangupBtn')
const config = {
iceServers: [
{ urls: 'stun:global.stun.twilio.com:3478?transport=udp' }
],
};
let peerConnection;
let socket, userId, toUserId;
userId = parseInt(Math.random()*10000);
document.getElementById('userId').innerText = '我的id:' + userId;
// 本地流和远端流
let localStream, remoteStream;
function requestConnect() {
toUserId = document.getElementById('toUserId').value
if(!toUserId){
alert('请输入对方id')
return false
}else if(!socket){
alert('请先打开websocket')
return false
}else if(toUserId == userId){
alert('自己不能和自己连接')
return false
}
//准备连接
startHandle().then(() => {
//发送给远端开启请求
socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': {'type': 'connect'}}))
})
}
//开启本地的媒体设备
async function startHandle() {
// 1.获取本地音视频流
// 调用 getUserMedia API 获取音视频流
let constraints = {
video: true,
audio: {
// 设置回音消除
noiseSuppression: true,
// 设置降噪
echoCancellation: true,
}
}
await navigator.mediaDevices.getUserMedia(constraints)
.then(gotLocalMediaStream)
.catch((err) => {
console.log('getUserMedia 错误', err);
//创建点对点连接对象
});
createConnection();
}
// getUserMedia 获得流后,将音视频流展示并保存到 localStream
function gotLocalMediaStream(mediaStream) {
localVideo.srcObject = mediaStream;
localStream = mediaStream;
callBtn.disabled = false;
}
function startWebsocket() {
toUserId = document.getElementById('toUserId').value
let webSocketUrl = 'wss://' + location.host + '/websocket/' + userId
if ('WebSocket' in window) {
// console.log(1)
socket = new WebSocket(webSocketUrl);
} else if ('MozWebSocket' in window) {
// console.log(2)
socket = new MozWebSocket(webSocketUrl);
}
// socket = new SockJS('https://' + location.host + '/websocket/' + userId);
//连接成功
socket.onopen = function (e) {
console.log('连接服务器成功!')
};
//server端请求关闭
socket.onclose = function (e) {
console.log('close')
alert(JSON.stringify(e))
};
//error
socket.onerror = function (e) {
console.error(e)
alert(JSON.stringify(e))
};
socket.onmessage = onmessage
}
//连接服务器
startWebsocket();
function onmessage(e) {
const json = JSON.parse(e.data)
const description = json.message
toUserId = json.userId
switch (description.type) {
case 'connect':
if(confirm(toUserId + '请求连接!')){
//准备连接
startHandle().then(() => {
socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': {'type': 'start'} }));
})
}
break;
case 'start':
//同意连接之后开始连接
startConnection()
break;
case 'offer':
peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {
}).catch((err) => {
console.log('local 设置远端描述信息错误', err);
});
peerConnection.createAnswer().then(function (answer) {
peerConnection.setLocalDescription(answer).then(() => {
console.log('设置本地answer成功!');
}).catch((err) => {
console.error('设置本地answer失败', err);
});
socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': answer }));
}).catch(e => {
console.error(e)
});
break;
case 'icecandidate':
// 创建 RTCIceCandidate 对象
let newIceCandidate = new RTCIceCandidate(description.icecandidate);
// 将本地获得的 Candidate 添加到远端的 RTCPeerConnection 对象中
peerConnection.addIceCandidate(newIceCandidate).then(() => {
console.log(`addIceCandidate 成功`);
}).catch((error) => {
console.log(`addIceCandidate 错误:\n` + `${error.toString()}.`);
});
break;
case 'answer':
peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {
console.log('设置remote answer成功!');
}).catch((err) => {
console.log('设置remote answer错误', err);
});
break;
default:
break;
}
}
function createConnection() {
peerConnection = new RTCPeerConnection(config)
if (localStream) {
// 视频轨道
const videoTracks = localStream.getVideoTracks();
// 音频轨道
const audioTracks = localStream.getAudioTracks();
// 判断视频轨道是否有值
if (videoTracks.length > 0) {
console.log(`使用的设备为: ${videoTracks[0].label}.`);
}
// 判断音频轨道是否有值
if (audioTracks.length > 0) {
console.log(`使用的设备为: ${audioTracks[0].label}.`);
}
localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, localStream)
})
}
// 监听返回的 Candidate
peerConnection.addEventListener('icecandidate', handleConnection);
// 监听 ICE 状态变化
peerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange)
//拿到流的时候调用
peerConnection.addEventListener('track', gotRemoteMediaStream);
}
//创建发起方会话描述对象(createOffer),设置本地SDP(setLocalDescription),并通过信令服务器发送到对等端,以启动与远程对等端的新WebRTC连接。
function startConnection() {
callBtn.disabled = true;
hangupBtn.disabled = false;
// 发送offer
peerConnection.createOffer().then(description => {
console.log(`本地创建offer返回的sdp:\n${description.sdp}`)
// 将 offer 保存到本地
peerConnection.setLocalDescription(description).then(() => {
console.log('local 设置本地描述信息成功');
// 本地设置描述并将它发送给远端
socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': description }));
}).catch((err) => {
console.log('local 设置本地描述信息错误', err)
});
})
.catch((err) => {
console.log('createdOffer 错误', err);
});
}
function hangupHandle() {
// 关闭连接并设置为空
peerConnection.close();
peerConnection = null;
hangupBtn.disabled = true;
callBtn.disabled = false;
localStream.getTracks().forEach((track) => {
track.stop()
})
}
// 3.端与端建立连接
function handleConnection(event) {
// 获取到触发 icecandidate 事件的 RTCPeerConnection 对象
// 获取到具体的Candidate
console.log("handleConnection")
const peerConnection = event.target;
const icecandidate = event.candidate;
if (icecandidate) {
socket.send(JSON.stringify({
'userId': userId,
'toUserId': toUserId,
'message': {
type: 'icecandidate',
icecandidate: icecandidate
}
}));
}
}
// 4.显示远端媒体流
function gotRemoteMediaStream(event) {
console.log('remote 开始接受远端流')
if (event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
remoteStream = event.streams[0];
}
}
function handleConnectionChange(event) {
const peerConnection = event.target;
console.log('ICE state change event: ', event);
console.log(`ICE state: ` + `${peerConnection.iceConnectionState}.`);
}
</script>
<style>
.video {
background-color: black;
height: 30vh;
}
</style>
再看看服务器代码,这里使用Java语言开发,spring boot框架,使用websocket转发信令。
websocket连接类
@ServerEndpoint("/websocket/{id}")
@Controller
@Slf4j
public class WebsocketController {
@OnOpen
public void onOpen(Session session, @PathParam(value = "id") String id) {
//获取连接的用户
log.info("加入session:" + id );
SocketManager.setSession(id, session);
}
@OnClose
public void onClose(Session session) {
log.info("移除不用的session:" + session.getId());
SocketManager.removeSession(session.getId());
}
//收到客户端信息
@OnMessage
public void onMessage(String message,Session session) throws IOException {
log.info("message,{}",message);
JSONObject jsonObject = JSON.parseObject(message);
SocketManager.sendMessage(jsonObject.getString("toUserId"),jsonObject.toJSONString());
}
//错误时调用
@OnError
public void onError(Session session, Throwable throwable) {
log.error("webSocket 错误,{}", throwable.getMessage());
SocketManager.removeSession(session.getId());
}
}
SocketManager类代码
public class SocketManager {
/**
* 客户端连接session集合 <id,Session></>
*/
private static Map<String,Session> sessionMap = new ConcurrentHashMap<String,Session>();
/**
* 存放新加入的session 便于以后对session管理
* @param userId 用户id
* @param session 用户session
*/
public synchronized static void setSession(String userId,Session session){
sessionMap.put(userId,session);
}
/**
* 根据session ID移除session
* @param sessionId
*/
public static void removeSession(String sessionId){
if(null == sessionId){
return;
}
sessionMap.forEach((k,v) -> {
if(sessionId.equals(v.getId())){
sessionMap.remove(k);
}
});
}
/**
* 给用户发送消息
* @param userId 用户id
* @param message 消息
*/
public static void sendMessage(String userId, String message) {
log.info("给用户发送消息,{},{}",userId,message);
Session session = sessionMap.get(userId);
if(session != null){
synchronized (session){
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.info("发送websocket消息是异常,{}",e);
}
}
}
}
}
最后看下实现效果打开页面,在另一台电脑打开或者浏览器新建一个标签页,输入对方的id点击建立连接就可以实现音视频通话了。