一、流程简述
客户端A、客户端B与信令服务器建立socket连接用于交换SDP信息
信令服务(Nest.js)
信令服务主要使用socket实现房间的概念,对房间内的用户进行信息的交换
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ cors: { origin: '*' } })
export class WsGateway {
@WebSocketServer()
server: Server;
// 用于存储房间房间和用于id
private users: Map<string, string> = new Map();
// 加入房间
@SubscribeMessage('joinRoom')
joinRoom(
@MessageBody() roomId: string,
@ConnectedSocket() client: Socket,
): void {
const previousRoom = this.users.get(client.id);
// 判断是否存在之前的房间、如果之前进入且没退出则退出
if (previousRoom) {
client.leave(previousRoom);
}
// 获取房间内信息
const roomClients = this.server.sockets.adapter.rooms.get(roomId);
// 限制一个房间不能超过两人,超出则加入失败
if (roomClients?.size === 2) {
return client.emit('error','加入失败')
}
client.join(roomId);
this.users.set(client.id, roomId);
// 通知房间内的用户,XX进入
client.to(roomId).emit('userJoined', client.id);
}
// 断开连接通知房间内的用户
disconnect(client: Socket): void {
const roomId = this.users.get(client.id);
if (roomId) {
client.to(roomId).emit('userLeft', client.id);
this.users.delete(client.id);
}
}
// 信令交换
@SubscribeMessage('signal')
receptionOfferAnswer(
@MessageBody() data: any,
@ConnectedSocket() client: Socket)
{
const roomClients = this.server.sockets.adapter.rooms.get(data.roomId);
if (roomClients && roomClients.size > 1) {
client.to(roomId).emit('reception', data.offer || data.answer);
}
}
}
客户端
客户端使用socket与信令服务建立连接,通过peerConnections.createOffer()和peerConnections.createAnswer()创建offer信令、answer信令。再通过交换彼此信令,建立点对点管道通讯。
<script lang="ts" setup>
import { io } from 'socket.io-client'
import { ref } from 'vue'
const socket = io('http://xxxx:9988')
const roomId = 'test-room' // 房间 ID
// 加入房间
socket.emit('joinRoom', roomId)
// 监听其他用户加入
socket.on('userJoined', id => {
// 其他用户加入创建offer
createConnection()
})
socket.on('reception', data => {
if (data.type === 'offer') {
peerConnections
.setRemoteDescription(new RTCSessionDescription(data))
.then(() => peerConnections.createAnswer())
.then(answer => peerConnections.setLocalDescription(answer))
.then(() => {
// 推送offer给房间内其他用户
socket.emit('offer', { roomId, answer: peerConnections.localDescription })
})
} else if (data.type === 'answer') {
// 根据answer信令建立连接
peerConnections.setRemoteDescription(new RTCSessionDescription(data))
}
})
// 创建 WebRTC 连接
const peerConnections = new RTCPeerConnection()
function createConnection() {
// 在这里创建 WebRTC 连接
peerConnections.createOffer().then(offer =>
peerConnections.setLocalDescription(offer).then(() => {
socket.emit('offer', {
roomId,
offer: peerConnections.localDescription
})
})
)
}
</script>
使用navigator.mediaDevices .getDisplayMedia()获取媒体流,peerConnections.addTrack()推送媒体流
const startScreenShare = () => {
navigator.mediaDevices
.getDisplayMedia({
audio: true,
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 60 }
}
})
.then(stream => {
// 推送媒体流
peerConnections.addTrack(stream.getVideoTracks()[0], stream)
})
.catch(e => {
console.error('未能获取媒体', e)
})
}