先看頁面代碼,這里使用的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點擊建立連接就可以實現音視頻通話了。
1612319194(1).jpg