feat:跟新livekit房间创建

This commit is contained in:
leilei
2025-09-26 17:58:42 +08:00
parent 121fc5ea19
commit af994f88de
15 changed files with 1052 additions and 41 deletions

View File

@@ -0,0 +1,683 @@
<template>
<div id="audio"></div>
<el-row>
<el-col :span="16">
<el-table :data="rooms.tableData" style="width: 100%">
<el-table-column prop="name" label="房间名称" />
<el-table-column prop="empty_timeout" label="空房间关闭时间" />
<el-table-column prop="creation_time" label="创建时间" />
<el-table-column prop="turn_password" label="口令" />
</el-table>
</el-col>
<el-col :span="8">
<div class="form" v-show="status">
<el-form :model="formModel" label-position="right" :label-width="90">
<el-form-item label="房间名称:">
<el-input v-model="formModel.room" placeholder="输入房间名称" style="width: 300px" />
</el-form-item>
<el-form-item label="用户编码:">
<el-input v-model="formModel.uid" placeholder="输入用户编码" style="width: 300px" />
</el-form-item>
<el-form-item label="选项:">
<el-radio-group v-model="formModel.creatRoom">
<el-radio label="0">
<span>创建房间</span>
</el-radio>
<el-radio label="1">
<span>加入房间</span>
</el-radio>
</el-radio-group>
</el-form-item>
<el-button type="primary" @click="joinRoomBtn">
{{ formModel.creatRoom === "0" ? "创建房间" : "加入房间" }}
</el-button>
</el-form>
</div>
</el-col>
</el-row>
<div v-if="!status" class="form">
<el-result icon="success" :title="formModel.room" sub-title="已加入房间" />
<el-input v-model="msgDate.some" placeholder="输入要发送的消息"></el-input>
<el-button @click="postMessage" type="success">发送文本消息</el-button>
<!-- 视频容器 -->
<div class="video-container">
<!-- 本地视频 -->
<div class="video-wrapper">
<h3>我的视频 ({{ formModel.uid }})</h3>
<video ref="localVideo" autoplay muted playsinline class="video-element"></video>
<div class="video-controls">
<el-button @click="toggleCamera" :type="cameraEnabled ? 'danger' : 'success'" size="small">
{{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
</el-button>
<el-button @click="toggleMicrophone" :type="microphoneEnabled ? 'danger' : 'success'" size="small">
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
</el-button>
<el-button @click="toggleScreenShare" :type="isScreenSharing ? 'danger' : 'primary'" size="small">
{{ isScreenSharing ? '停止共享' : '共享屏幕' }}
</el-button>
</div>
</div>
<!-- 远程视频 -->
<div class="video-wrapper" v-for="participant in remoteParticipants" :key="participant.identity">
<h3>{{ participant.identity }}</h3>
<div class="video-tracks">
<video
v-for="videoTrack in participant.videoTracks"
:key="videoTrack.sid"
:ref="el => setRemoteVideo(el, videoTrack)"
autoplay
playsinline
class="video-element">
</video>
</div>
<div class="participant-info">
<span>音频: {{ participant.audioEnabled ? '开启' : '关闭' }}</span>
<span>视频: {{ participant.videoEnabled ? '开启' : '关闭' }}</span>
</div>
</div>
</div>
<!-- 显示连接状态 -->
<div v-if="connectionStatus" :class="['status', connectionStatus.type]">
{{ connectionStatus.message }}
</div>
<!-- 房间信息 -->
<div class="room-info" v-if="!status">
<h4>房间信息</h4>
<p>房间名称: {{ room.name }}</p>
<p>参与者数量: {{ participantCount }}</p>
<p>连接状态: {{ room.connectionState }}</p>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted, onUnmounted, nextTick, computed } from "vue";
import { ElMessage } from 'element-plus';
import { getRoomToken, getRoomList } from "@/api/conferencingRoom.js"
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
// LiveKit 服务器配置 - 确保使用正确的服务器地址
const wsURL = "wss://meeting.cnsdt.com:443";
// 响应式数据
const formModel = reactive({
room: 'thehome',
uid: 'xtqxk',
creatRoom: '0'
});
const status = ref(true);
const connectionStatus = ref(null);
const rooms = reactive({ tableData: [] });
const msgDate = reactive({ some: '' });
// 视频相关引用和数据
const localVideo = ref(null);
const cameraEnabled = ref(false);
const microphoneEnabled = ref(false);
const isScreenSharing = ref(false);
const remoteParticipants = ref([]);
// 创建 Room 实例
const room = new Room({
// 启用自适应流,根据网络状况自动调整视频质量
adaptiveStream: true,
// 启用动态投射,只传输当前可见的视频流以节省带宽
dynacast: true,
// 配置视频捕获默认值
videoCaptureDefaults: {
resolution: { width: 1280, height: 720 }
},
// 配置发布选项
publishDefaults: {
// 配置屏幕分享
screenShareEncoding: {
maxBitrate: 3_000_000,// 最大比特率 3 Mbps
maxFramerate: 30, // 最大帧率 30 fps
},
// 配置视频编码
videoEncoding: {
maxBitrate: 2_500_000,
maxFramerate: 30,
},
}
});
// 计算属性
const participantCount = computed(() => {
return remoteParticipants.value.length + 1; // 包括自己
});
// 设置事件监听器
function setupRoomListeners() {
// 清除现有监听器
room.removeAllListeners();
room
.on(RoomEvent.Connected, handleConnected) // 连接成功
.on(RoomEvent.Disconnected, handleDisconnected)// 连接断开
.on(RoomEvent.Reconnected, handleReconnected)// 重连成功
.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)// 订阅远程轨道
.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)// 取消订阅远程轨道
.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged)// 活跃说话者变化
.on(RoomEvent.DataReceived, handleDataReceived) // 接收到自定义数据
.on(RoomEvent.ConnectionStateChanged, handleConnectionStateChanged)// 连接状态变化
.on(RoomEvent.ParticipantConnected, handleParticipantConnected)// 新参与者加入
.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)// 参与者离开
.on(RoomEvent.LocalTrackPublished, handleLocalTrackPublished)// 本地轨道发布成功
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished)// 本地轨道取消发布
.on(RoomEvent.TrackMuted, handleTrackMuted) // 轨道静音
.on(RoomEvent.TrackUnmuted, handleTrackUnmuted);// 轨道取消静音
}
// 事件处理函数
async function handleConnected() {
console.log("成功连接到房间:", room.name);
status.value = false;
connectionStatus.value = {
type: 'success',
message: `已成功连接到房间: ${room.name}`
};
ElMessage.success('已成功连接到房间');
// 初始化远程参与者列表
updateRemoteParticipants();
// 自动开启摄像头(仅创建房间时)
if (formModel.creatRoom === "0") {
try {
await enableCamera();
ElMessage.success('摄像头已自动开启');
} catch (error) {
console.warn('自动开启摄像头失败:', error);
ElMessage.warning('自动开启摄像头失败,请手动开启');
}
}
// 监听所有现有远程参与者
room.remoteParticipants.forEach(participant => {
setupParticipantListeners(participant);
});
}
function handleDisconnected(reason) {
console.log("断开连接:", reason);
status.value = true;
cameraEnabled.value = false;
microphoneEnabled.value = false;
isScreenSharing.value = false;
remoteParticipants.value = [];
connectionStatus.value = {
type: 'error',
message: `连接已断开: ${reason}`
};
ElMessage.error('连接已断开');
}
function handleReconnected() {
console.log("已重新连接");
connectionStatus.value = {
type: 'success',
message: '已重新连接到房间'
};
updateRemoteParticipants();
ElMessage.success('已重新连接到房间');
}
// 处理轨道订阅事件
function handleTrackSubscribed(track, publication, participant) {
console.log("轨道已订阅:", track.kind, "来自:", participant.identity);
if (track.kind === Track.Kind.Video || track.kind === Track.Kind.Audio) {
updateRemoteParticipants();
nextTick(() => {
if (track.kind === Track.Kind.Video) {
attachVideoTrack(track, participant.identity);
}
});
}
}
function handleTrackUnsubscribed(track, publication, participant) {
console.log("轨道取消订阅:", track.kind, participant.identity);
updateRemoteParticipants();
}
function handleParticipantConnected(participant) {
console.log("参与者连接:", participant.identity);
console.log("参与者连接+++:", participant);
setupParticipantListeners(participant);
updateRemoteParticipants();
}
function handleParticipantDisconnected(participant) {
console.log("参与者断开连接:", participant.identity);
updateRemoteParticipants();
}
function handleLocalTrackPublished(publication) {
console.log("本地轨道发布:", publication.kind);
if (publication.kind === Track.Kind.Video && publication.track) {
attachLocalVideoTrack(publication.track);
}
}
function handleLocalTrackUnpublished(publication) {
console.log("本地轨道取消发布:", publication.kind);
if (publication.kind === Track.Kind.Video) {
cameraEnabled.value = false;
} else if (publication.kind === Track.Kind.Audio) {
microphoneEnabled.value = false;
}
}
function handleTrackMuted(publication, participant) {
console.log("轨道静音:", publication.kind, participant.identity);
updateRemoteParticipants();
}
function handleTrackUnmuted(publication, participant) {
console.log("轨道取消静音:", publication.kind, participant.identity);
updateRemoteParticipants();
}
function setupParticipantListeners(participant) {
participant
.on(ParticipantEvent.TrackSubscribed, (track, publication) => {
handleTrackSubscribed(track, publication, participant);
})
.on(ParticipantEvent.TrackUnsubscribed, (track, publication) => {
handleTrackUnsubscribed(track, publication, participant);
})
.on(ParticipantEvent.TrackMuted, (publication) => {
handleTrackMuted(publication, participant);
})
.on(ParticipantEvent.TrackUnmuted, (publication) => {
handleTrackUnmuted(publication, participant);
});
}
function handleActiveSpeakersChanged(speakers) {
console.log("活跃说话者变化:", speakers.map(s => s.identity));
}
function handleDataReceived(payload, participant, kind) {
try {
const decoder = new TextDecoder();
const strData = decoder.decode(payload);
console.log("接收到消息:", strData, "来自:", participant.identity);
ElMessage.info(`收到消息 from ${participant.identity}: ${strData}`);
} catch (error) {
console.error('处理接收消息失败:', error);
}
}
function handleConnectionStateChanged(state) {
console.log("连接状态变化:", state);
connectionStatus.value = {
type: 'info',
message: `连接状态: ${state}`
};
}
// 更新远程参与者列表
function updateRemoteParticipants() {
try {
const participants = [];
if (room && room.remoteParticipants) {
console.log(room.remoteParticipants,'room.remoteParticipants+++++')
// 使用更安全的方式遍历远程参与者
for (const [identity, participant] of Object.entries(room.remoteParticipants)) {
if (participant && participant.identity !== room.localParticipant?.identity) {
const videoTracks = [];
// 获取视频轨道
if (participant.videoTracks) {
for (const [trackId, publication] of Object.entries(participant.videoTracks)) {
if (publication && publication.track) {
videoTracks.push({
sid: publication.trackSid,
track: publication.track,
kind: publication.kind
});
}
}
}
participants.push({
identity: participant.identity,
videoTracks: videoTracks,
audioEnabled: !participant.isMicrophoneEnabled,
videoEnabled: !participant.isCameraEnabled
});
}
}
}
remoteParticipants.value = participants;
console.log(participants,'participants--+++--')
} catch (error) {
console.error('更新远程参与者列表失败:', error);
remoteParticipants.value = [];
}
}
// 附加本地视频轨道
function attachLocalVideoTrack(track) {
if (localVideo.value && track) {
try {
const element = track.attach();
if (element && element.srcObject) {
localVideo.value.srcObject = element.srcObject;
}
} catch (error) {
console.error('附加本地视频轨道失败:', error);
}
}
}
// 附加远程视频轨道
function attachVideoTrack(track, participantIdentity) {
if (track && track.kind === Track.Kind.Video) {
try {
const element = track.attach();
console.log('远程视频轨道附加成功:', participantIdentity);
} catch (error) {
console.error('附加远程视频轨道失败:', error);
}
}
}
// 设置远程视频引用
function setRemoteVideo(el, videoTrack) {
if (el && videoTrack && videoTrack.track) {
try {
const element = videoTrack.track.attach();
if (element && element.srcObject) {
el.srcObject = element.srcObject;
}
} catch (error) {
console.error('设置远程视频失败:', error);
}
}
}
// 开启摄像头
async function enableCamera() {
try {
await room.localParticipant.setCameraEnabled(true);
cameraEnabled.value = true;
return true;
} catch (error) {
console.error('开启摄像头失败:', error);
cameraEnabled.value = false;
throw error;
}
}
// 切换摄像头状态
async function toggleCamera() {
try {
if (cameraEnabled.value) {
await room.localParticipant.setCameraEnabled(false);
cameraEnabled.value = false;
ElMessage.info('摄像头已关闭');
} else {
await room.localParticipant.setCameraEnabled(true);
cameraEnabled.value = true;
ElMessage.success('摄像头已开启');
}
} catch (error) {
console.error('切换摄像头失败:', error);
ElMessage.error('切换摄像头失败: ' + error.message);
}
}
// 切换麦克风状态
async function toggleMicrophone() {
try {
if (microphoneEnabled.value) {
await room.localParticipant.setMicrophoneEnabled(false);
microphoneEnabled.value = false;
ElMessage.info('麦克风已关闭');
} else {
await room.localParticipant.setMicrophoneEnabled(true);
microphoneEnabled.value = true;
ElMessage.success('麦克风已开启');
}
} catch (error) {
console.error('切换麦克风失败:', error);
ElMessage.error('切换麦克风失败: ' + error.message);
}
}
// 切换屏幕共享
async function toggleScreenShare() {
try {
if (isScreenSharing.value) {
await room.localParticipant.setScreenShareEnabled(false);
isScreenSharing.value = false;
ElMessage.info('屏幕共享已停止');
} else {
await room.localParticipant.setScreenShareEnabled(true);
isScreenSharing.value = true;
ElMessage.success('屏幕共享已开始');
}
} catch (error) {
console.error('切换屏幕共享失败:', error);
ElMessage.error('切换屏幕共享失败: ' + error.message);
}
}
// 获取房间列表
async function getRoomsList() {
try {
const res = await getRoomList();
console.log('房间列表:', res);
if (res.data.rooms) {
rooms.tableData = res.data.rooms;
}
} catch (error) {
console.error('获取房间列表失败:', error);
ElMessage.error('获取房间列表失败');
}
}
// 加入房间
async function joinRoomBtn() {
try {
// 验证输入
if (!formModel.room.trim() || !formModel.uid.trim()) {
ElMessage.error('请填写房间名称和用户编码');
return;
}
connectionStatus.value = { type: 'info', message: '正在获取 token...' };
// 获取 token
const res = await getRoomToken(formModel);
const token = res.data.access_token;
if (!token) {
throw new Error('获取 token 失败');
}
connectionStatus.value = { type: 'info', message: '正在连接房间...' };
// 设置事件监听
setupRoomListeners();
// 连接房间 - 添加更详细的配置
await room.connect(wsURL, token, {
// 自动订阅所有轨道
autoSubscribe: true,
});
// 连接成功后获取最新房间列表
await getRoomsList();
} catch (error) {
console.error('连接失败:', error);
connectionStatus.value = {
type: 'error',
message: `连接失败: ${error.message}`
};
ElMessage.error(`连接失败: ${error.message}`);
// 重置状态
status.value = true;
}
}
// 发送消息
function postMessage() {
try {
if (!msgDate.some.trim()) {
ElMessage.warning('请输入消息内容');
return;
}
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify({
message: msgDate.some,
timestamp: new Date().toISOString(),
from: formModel.uid
}));
room.localParticipant.publishData(data, { reliable: true });
ElMessage.success('消息发送成功');
msgDate.some = '';
} catch (error) {
console.error('发送消息失败:', error);
ElMessage.error('发送消息失败');
}
}
// 组件卸载时断开连接
onUnmounted(() => {
if (room) {
room.disconnect();
}
});
onMounted(() => {
getRoomsList();
setupRoomListeners();
});
</script>
<style>
.form {
margin: 20px auto;
width: 100%;
max-width: 1200px;
padding: 20px;
}
.video-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
justify-content: center;
}
.video-wrapper {
border: 2px solid #e4e7ed;
border-radius: 8px;
padding: 15px;
background: #f5f7fa;
min-width: 320px;
max-width: 400px;
}
.video-wrapper h3 {
margin: 0 0 10px 0;
text-align: center;
color: #606266;
font-size: 14px;
}
.video-element {
width: 100%;
height: 225px;
background: #000;
border-radius: 4px;
object-fit: cover;
}
.video-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 10px;
}
.participant-info {
display: flex;
justify-content: space-around;
margin-top: 8px;
font-size: 12px;
color: #909399;
}
.video-tracks {
display: flex;
flex-direction: column;
gap: 10px;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
text-align: center;
}
.status.success {
background-color: #f0f9ff;
color: #67c23a;
border: 1px solid #b3e19d;
}
.status.error {
background-color: #fef0f0;
color: #f56c6c;
border: 1px solid #fbc4c4;
}
.status.info {
background-color: #f4f4f5;
color: #909399;
border: 1px solid #d3d4d6;
}
.room-info {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #409eff;
}
.room-info h4 {
margin: 0 0 10px 0;
color: #409eff;
}
.room-info p {
margin: 5px 0;
font-size: 14px;
}
</style>