记录一下,利用webrtc、socket.io、nestjs实现视频通话,这里只支持两个人通话
server
server必备包
shell
pnpm add @nestjs/platform-socket.io @nestjs/websockets
server源码
js
// socket.gateway.ts
import { WebSocketGateway, SubscribeMessage, MessageBody, OnGatewayConnection, OnGatewayInit, OnGatewayDisconnect, ConnectedSocket, WebSocketServer } from '@nestjs/websockets';
import { SocketService } from './socket.service';
import { Server } from 'socket.io'
@WebSocketGateway({ transports: ['websocket'], cors: '*'})
export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
@WebSocketServer()
server: Server
user: Map<string, string>
room: string
constructor(private readonly socketService: SocketService) {
this.user = new Map()
this.room = ''
}
afterInit(server: any) {
}
handleConnection(client: any, ...args: any[]) {
let room = client.handshake.query.room
let username = client.handshake.query.username
if(this.user.size === 2) return
if (room) {
client.join(room)
client.in(room).emit('message', username + '已加入房间'+room)
this.user.set(client.id, username)
this.room = room
} else {
client.emit('message', client.id)
}
}
handleDisconnect(client: any) {
console.log('断开连接')
if (this.room) {
this.server.socketsLeave(this.room)
this.server.disconnectSockets(true)
this.room = null
}
}
@SubscribeMessage('chat')
chat(@MessageBody() message: { room: string, msg: string }, @ConnectedSocket() client) {
client.to(message.room).emit('message', message.msg, this.user.get(client.id))
}
@SubscribeMessage('offer')
offer(@MessageBody() message: { room: string, offer: any }, @ConnectedSocket() client) {
client.to(message.room).emit('offer', message.offer)
}
@SubscribeMessage('answer')
answer(@MessageBody() message: { room: string, answer: any }, @ConnectedSocket() client) {
client.to(message.room).emit('answer', message.answer)
}
@SubscribeMessage('candidate')
candidate(@MessageBody() message: { room: string, candidate: any }, @ConnectedSocket() client) {
client.to(message.room).emit('candidate', message.candidate)
}
@SubscribeMessage('videoCall')
videoCall(@MessageBody() message: { room: string }, @ConnectedSocket() client) {
client.to(message.room).emit('videoCall')
}
@SubscribeMessage('acceptVideoCall')
acceptVideoCall(@MessageBody() message: { room: string }, @ConnectedSocket() client) {
client.to(message.room).emit('acceptVideoCall')
}
}
client端
client必备包
shell
pnpm add socket.io-client
client源码
vue
<script setup lang="ts">
import { ref } from 'vue';
import { io } from 'socket.io-client'
import type { Socket } from 'socket.io-client'
let socket: Socket | null = null
const room = ref('test')
const username = ref()
const isInRoom = ref(false)
const isConnect = ref(false)
let localStream : any = null
const videoRef = ref()
const remoteVideoRef = ref()
const receiveBoxRef = ref()
let localConnection: RTCPeerConnection | null = null
let localChannel: RTCDataChannel | null = null
const connectSocket = function(room: string, username: string) {
socket = io('https://192.168.3.22:3000', {
transports: ['websocket'],
query: {
room,
username
}
})
socket.on('connect', () => {
isInRoom.value = true
console.log('socket连接成功')
})
socket.on('disconnect', () => {
console.log('断开连接')
socket?.disconnect()
close()
})
socket.on('message', (msg, username) => {
console.log(msg, username)
})
socket.on('offer', async (offer) => {
await receiveOffer(offer, socket!)
})
socket.on('answer',async (answer) => {
await receiveAnswer(answer, localConnection!)
})
socket.on('candidate', async (candidate) => {
console.log('收到candidate', candidate)
if (localConnection) {
await localConnection.addIceCandidate(candidate)
}
})
socket.on('videoCall',async () => {
if (confirm('是否接受视频通话')) {
localStream = await createLocalStream()
remoteVideoRef.value.play()
socket?.emit('acceptVideoCall', { room })
}
})
socket.on('acceptVideoCall',async () => {
createRTC()
})
}
function createLocalDataChannel(localConnection: RTCPeerConnection, channel?: RTCDataChannel) {
// dataChannel
localChannel = channel ? channel : localConnection.createDataChannel('sendChannel')
localChannel.onopen = function() {
console.log('数据通道打开')
}
localChannel.onclose = function() {
console.log('数据通道关闭')
}
localChannel.onmessage = function(e) {
console.log('数据通道收到消息', e.data)
const data = JSON.parse(e.data)
pushMessage(data.msg, data.username)
}
}
async function sendAnswer(localConnection: RTCPeerConnection, socket: Socket) {
const answer = await localConnection.createAnswer()
await localConnection.setLocalDescription(answer)
socket.emit('answer', { answer, room: room.value })
}
async function receiveAnswer(answer: RTCSessionDescriptionInit, localConnection: RTCPeerConnection) {
await localConnection.setRemoteDescription(answer)
isConnect.value = true
}
async function sendOffer(localConnection: RTCPeerConnection, socket: Socket) {
const offer = await localConnection.createOffer()
await localConnection.setLocalDescription(offer)
// 通过socket发送offer 带上房间号
socket.emit('offer', { offer, room: room.value })
}
async function receiveOffer(offer: RTCSessionDescriptionInit, socket: Socket) {
localConnection = await connectPeers(socket)
// 如果存在offer则是收到offer连接远端
await localConnection.setRemoteDescription(offer)
await sendAnswer(localConnection, socket)
isConnect.value = true
}
const connectPeers = async function(socket: Socket) : Promise<RTCPeerConnection> {
let localConnection = new RTCPeerConnection()
createLocalDataChannel(localConnection)
localConnection.ondatachannel = function(e) {
createLocalDataChannel(localConnection, e.channel)
}
startVideoCall1(localConnection)
localConnection.ontrack = (e) => {
console.log('收到track流', e)
remoteVideoRef.value.srcObject = e.streams[0]
remoteVideoRef.value.oncanplay = () => remoteVideoRef.value.play()
}
localConnection.onicecandidate = (e) => {
if (e.candidate) {
socket.emit('candidate', { candidate: e.candidate, room: room.value })
}
}
return localConnection
}
const joinRoom = () => {
if (!room.value && !username.value) {
alert('请检查输入项')
return
}
connectSocket(room.value, username.value)
}
const outRoom = () => {
isInRoom.value = false
socket?.disconnect()
}
const createRTC = async () => {
localConnection = await connectPeers(socket!)
sendOffer(localConnection, socket!)
}
async function createLocalStream() {
const constraints = { video: true, audio: true }
const localStream = await navigator.mediaDevices.getUserMedia(constraints)
// 我方视频
videoRef.value.oncanplay = () => videoRef.value.play()
videoRef.value.srcObject = localStream
return localStream
}
const startVideoCall = async () => {
localStream = await createLocalStream()
socket?.emit('videoCall', { room: room.value })
}
const startVideoCall1 = async (localConnection: RTCPeerConnection) => {
// 添加rtc流
localStream.getTracks().forEach((track: any) => {
console.log(track)
localConnection!.addTrack(track, localStream)
})
}
function close() {
localConnection?.close()
localStream.getTracks().forEach(function (track: any) {
track.stop();
});
socket = null
localStream = null
localConnection = null
localChannel = null
isConnect.value = false
isInRoom.value = false
}
const messages = ref<{ msg: string, username: string}[]>([])
function pushMessage(message: string, username: string) {
messages.value.push({ msg: message, username })
}
const msg = ref('')
const sendMessage = () => {
localChannel?.send(JSON.stringify({ msg: msg.value, username: username.value }))
pushMessage(msg.value, username.value)
msg.value = ''
}
</script>
<template>
<main>
<h1>聊天</h1>
<div>
<input type="text" :disabled="isInRoom" v-model="username" placeholder="用户名">
<input type="text" :disabled="isInRoom" v-model="room" placeholder="房间号">
<button @click="joinRoom">加入房间</button>
<button @click="outRoom">退出房间</button>
</div>
<div v-if="isInRoom && !isConnect">
<button @click="startVideoCall">开启视频通话</button>
</div>
<div class="box" ref="receiveBoxRef">
<p>消息</p>
<template v-for="msg in messages">
<p v-if="msg.username === username" style="text-align: right;">
{{ msg.msg }}
<span style="border: 1px solid orange;">{{ msg.username }}</span>
</p>
<p v-else>
<span style="border: 1px solid greenyellow;">{{ msg.username }}</span>
{{ msg.msg }}
</p>
</template>
</div>
<div>
<input type="text" v-model="msg" placeholder="消息">
<button @click="sendMessage">发送</button>
</div>
<video class="video1" :autoplay="false" ref="videoRef"></video>
<video class="video2" :autoplay="false" ref="remoteVideoRef"></video>
</main>
</template>
<style>
.box {
width: 300px;
height: 300px;
border: 1px solid black;
}
.video1 {
width: 100px;
height: 100px;
border: 1px solid black;
}
.video2 {
width: 300px;
height: 300px;
border: 1px solid black;
}
</style>
问题
- 视频流需在rtc连接时一起开启,否则rtc的ontrack不会触发