feat:更新协同合作
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- 音频元素 -->
|
||||||
|
<audio ref="localAudio" autoplay muted class="audio-element"></audio>
|
||||||
<div id="audio"></div>
|
<div id="audio"></div>
|
||||||
<div class="livekit-container">
|
<div class="livekit-container">
|
||||||
<div v-if="!status" class="meeting-container">
|
<div v-if="!status" class="meeting-container">
|
||||||
@@ -49,14 +51,17 @@
|
|||||||
<div class="participant-header">
|
<div class="participant-header">
|
||||||
<h3>我的视频 ({{ hostUid }})</h3>
|
<h3>我的视频 ({{ hostUid }})</h3>
|
||||||
<div class="status-indicator">
|
<div class="status-indicator">
|
||||||
<span class="status-dot"></span>
|
<span class="status-dot" :class="{ 'speaking': isLocalSpeaking }"></span>
|
||||||
<span>在线</span>
|
<span>{{ microphoneEnabled ? '音频在线' : '音频离线' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-wrapper">
|
<div class="video-wrapper">
|
||||||
<video ref="localVideo" autoplay muted playsinline class="video-element"></video>
|
<video ref="localVideo" autoplay muted playsinline class="video-element"></video>
|
||||||
<div class="video-overlay">
|
<div class="video-overlay">
|
||||||
<span class="participant-name">{{ hostUid }}</span>
|
<span class="participant-name">{{ hostUid }}</span>
|
||||||
|
<span class="audio-indicator" :class="{ 'muted': !microphoneEnabled }">
|
||||||
|
<i :class="microphoneEnabled ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 视频占位符 -->
|
<!-- 视频占位符 -->
|
||||||
<div v-if="!cameraEnabled" class="video-placeholder">
|
<div v-if="!cameraEnabled" class="video-placeholder">
|
||||||
@@ -71,8 +76,8 @@
|
|||||||
<div class="participant-header">
|
<div class="participant-header">
|
||||||
<h3>{{ participant.identity }}</h3>
|
<h3>{{ participant.identity }}</h3>
|
||||||
<div class="status-indicator">
|
<div class="status-indicator">
|
||||||
<span class="status-dot"></span>
|
<span class="status-dot" :class="{ 'speaking': participant.isSpeaking }"></span>
|
||||||
<span>在线</span>
|
<span>{{ participant.audioEnabled ? '音频在线' : '音频离线' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-wrapper">
|
<div class="video-wrapper">
|
||||||
@@ -97,12 +102,22 @@
|
|||||||
<span v-if="participant.hasScreenTrack" class="screen-sharing-indicator">
|
<span v-if="participant.hasScreenTrack" class="screen-sharing-indicator">
|
||||||
<i class="el-icon-monitor"></i> 正在共享
|
<i class="el-icon-monitor"></i> 正在共享
|
||||||
</span>
|
</span>
|
||||||
|
<span class="audio-indicator" :class="{ 'muted': !participant.audioEnabled }">
|
||||||
|
<i :class="participant.audioEnabled ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 远程参与者音频元素 -->
|
||||||
|
<audio
|
||||||
|
:ref="el => setParticipantAudioRef(el, participant.identity)"
|
||||||
|
autoplay
|
||||||
|
class="participant-audio">
|
||||||
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 固定在底部的控制按钮 -->
|
<!-- 固定在底部的控制按钮 -->
|
||||||
<div class="fixed-controls">
|
<div class="fixed-controls">
|
||||||
<div class="controls-container">
|
<div class="controls-container">
|
||||||
@@ -136,35 +151,30 @@ import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoomStore } from '@/stores/modules/room.js'
|
import { useRoomStore } from '@/stores/modules/room.js'
|
||||||
import useUserStore from '@/stores/modules/user'
|
import useUserStore from '@/stores/modules/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// LiveKit 服务器配置
|
// LiveKit 服务器配置
|
||||||
const wsURL = "wss://meeting.cnsdt.com:443";
|
const wsURL = "wss://meeting.cnsdt.com:443";
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
// const formModel = reactive({
|
|
||||||
// room: 'thehome',
|
|
||||||
// uid: 'xtqxk',
|
|
||||||
// creatRoom: '0'
|
|
||||||
// });
|
|
||||||
|
|
||||||
const hostUid = ref('')
|
const hostUid = ref('')
|
||||||
|
|
||||||
|
|
||||||
const status = ref(true);
|
const status = ref(true);
|
||||||
|
|
||||||
// 视频相关引用和数据
|
// 音视频相关引用和数据
|
||||||
const localVideo = ref(null);
|
const localVideo = ref(null);
|
||||||
|
const localAudio = ref(null);
|
||||||
const cameraEnabled = ref(false);
|
const cameraEnabled = ref(false);
|
||||||
const microphoneEnabled = ref(false);
|
const microphoneEnabled = ref(false);
|
||||||
const isScreenSharing = ref(false);
|
const isScreenSharing = ref(false);
|
||||||
|
const isLocalSpeaking = ref(false);
|
||||||
|
|
||||||
// 远程参与者管理
|
// 远程参与者管理
|
||||||
const remoteParticipants = ref(new Map());
|
const remoteParticipants = ref(new Map());
|
||||||
// 视频元素引用映射
|
|
||||||
const videoElementsMap = ref(new Map());
|
const videoElementsMap = ref(new Map());
|
||||||
|
const audioElementsMap = ref(new Map());
|
||||||
|
|
||||||
// 屏幕共享相关
|
// 屏幕共享相关
|
||||||
const screenSharingUser = ref('');
|
const screenSharingUser = ref('');
|
||||||
@@ -188,6 +198,12 @@ const hasActiveScreenShare = computed(() => {
|
|||||||
const room = new Room({
|
const room = new Room({
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
dynacast: true,
|
dynacast: true,
|
||||||
|
// 音频捕获配置
|
||||||
|
audioCaptureDefaults: {
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
autoGainControl: true,
|
||||||
|
},
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
resolution: { width: 1280, height: 720 }
|
resolution: { width: 1280, height: 720 }
|
||||||
},
|
},
|
||||||
@@ -200,6 +216,12 @@ const room = new Room({
|
|||||||
maxBitrate: 2_500_000,
|
maxBitrate: 2_500_000,
|
||||||
maxFramerate: 30,
|
maxFramerate: 30,
|
||||||
},
|
},
|
||||||
|
// 音频发布配置
|
||||||
|
audioEncoding: {
|
||||||
|
maxBitrate: 64000, // 64kbps for audio
|
||||||
|
},
|
||||||
|
dtx: true, // 不连续传输,节省带宽
|
||||||
|
red: true, // 冗余编码,提高抗丢包能力
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,6 +249,19 @@ function setParticipantVideoRef(el, identity, type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置音频元素引用
|
||||||
|
function setParticipantAudioRef(el, identity) {
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
audioElementsMap.value.set(identity, el);
|
||||||
|
|
||||||
|
// 如果已经有音频轨道数据,立即附加
|
||||||
|
const participantData = remoteParticipants.value.get(identity);
|
||||||
|
if (participantData && participantData.audioTrack) {
|
||||||
|
attachTrackToAudio(el, participantData.audioTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 设置屏幕共享视频引用
|
// 设置屏幕共享视频引用
|
||||||
function setScreenShareVideoRef(el) {
|
function setScreenShareVideoRef(el) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -274,15 +309,13 @@ async function handleConnected() {
|
|||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动开启摄像头(仅创建房间时)
|
// 自动开启麦克风
|
||||||
// if (formModel.creatRoom === "0") {
|
try {
|
||||||
// try {
|
await enableMicrophone();
|
||||||
// await enableCamera();
|
ElMessage.success('麦克风已自动开启');
|
||||||
// ElMessage.success('摄像头已自动开启');
|
} catch (error) {
|
||||||
// } catch (error) {
|
console.warn('自动开启麦克风失败:', error);
|
||||||
// console.warn('自动开启摄像头失败:', error);
|
}
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDisconnected(reason) {
|
function handleDisconnected(reason) {
|
||||||
@@ -293,6 +326,7 @@ function handleDisconnected(reason) {
|
|||||||
isScreenSharing.value = false;
|
isScreenSharing.value = false;
|
||||||
remoteParticipants.value.clear();
|
remoteParticipants.value.clear();
|
||||||
videoElementsMap.value.clear();
|
videoElementsMap.value.clear();
|
||||||
|
audioElementsMap.value.clear();
|
||||||
screenSharingUser.value = '';
|
screenSharingUser.value = '';
|
||||||
activeScreenShareTrack.value = null;
|
activeScreenShareTrack.value = null;
|
||||||
|
|
||||||
@@ -308,7 +342,8 @@ function handleReconnected() {
|
|||||||
function handleTrackSubscribed(track, publication, participant) {
|
function handleTrackSubscribed(track, publication, participant) {
|
||||||
console.log("轨道已订阅:", track.kind, "轨道来源:", publication.source, "来自:", participant.identity);
|
console.log("轨道已订阅:", track.kind, "轨道来源:", publication.source, "来自:", participant.identity);
|
||||||
|
|
||||||
if (track && track.kind === Track.Kind.Video) {
|
if (track) {
|
||||||
|
if (track.kind === Track.Kind.Video) {
|
||||||
// 更新参与者轨道信息
|
// 更新参与者轨道信息
|
||||||
updateParticipantTrack(participant, publication.source, track);
|
updateParticipantTrack(participant, publication.source, track);
|
||||||
|
|
||||||
@@ -319,6 +354,11 @@ function handleTrackSubscribed(track, publication, participant) {
|
|||||||
|
|
||||||
// 立即附加到视频元素(如果元素已存在)
|
// 立即附加到视频元素(如果元素已存在)
|
||||||
attachTrackToParticipantVideo(participant.identity, publication.source, track);
|
attachTrackToParticipantVideo(participant.identity, publication.source, track);
|
||||||
|
} else if (track.kind === Track.Kind.Audio) {
|
||||||
|
// 处理音频轨道订阅
|
||||||
|
updateParticipantAudioTrack(participant, track);
|
||||||
|
attachTrackToParticipantAudio(participant.identity, track);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新参与者的轨道状态
|
// 更新参与者的轨道状态
|
||||||
@@ -336,6 +376,10 @@ function handleTrackUnsubscribed(track, publication, participant) {
|
|||||||
if (publication.source === Track.Source.ScreenShare) {
|
if (publication.source === Track.Source.ScreenShare) {
|
||||||
updateScreenShareState(participant, null);
|
updateScreenShareState(participant, null);
|
||||||
}
|
}
|
||||||
|
} else if (track.kind === Track.Kind.Audio) {
|
||||||
|
// 处理音频轨道取消订阅
|
||||||
|
removeParticipantAudioTrack(participant);
|
||||||
|
detachTrackFromParticipantAudio(participant.identity);
|
||||||
}
|
}
|
||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
}
|
}
|
||||||
@@ -363,7 +407,8 @@ function handleParticipantDisconnected(participant) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleLocalTrackPublished(publication) {
|
function handleLocalTrackPublished(publication) {
|
||||||
if (publication.kind === Track.Kind.Video && publication.track) {
|
if (publication.track) {
|
||||||
|
if (publication.kind === Track.Kind.Video) {
|
||||||
if (publication.source === Track.Source.Camera) {
|
if (publication.source === Track.Source.Camera) {
|
||||||
attachLocalVideoTrack(publication.track);
|
attachLocalVideoTrack(publication.track);
|
||||||
} else if (publication.source === Track.Source.ScreenShare) {
|
} else if (publication.source === Track.Source.ScreenShare) {
|
||||||
@@ -374,6 +419,11 @@ function handleLocalTrackPublished(publication) {
|
|||||||
attachTrackToVideo(screenShareVideo.value, publication.track);
|
attachTrackToVideo(screenShareVideo.value, publication.track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (publication.kind === Track.Kind.Audio) {
|
||||||
|
// 本地音频轨道发布
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
console.log('本地音频轨道已发布');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +477,11 @@ function setupParticipantListeners(participant) {
|
|||||||
|
|
||||||
// 处理活动说话者变化事件
|
// 处理活动说话者变化事件
|
||||||
function handleActiveSpeakersChanged(speakers) {
|
function handleActiveSpeakersChanged(speakers) {
|
||||||
|
// 更新本地说话状态
|
||||||
|
const localIsSpeaking = speakers.some(speaker => speaker.identity === room.localParticipant.identity);
|
||||||
|
isLocalSpeaking.value = localIsSpeaking;
|
||||||
|
|
||||||
|
// 更新远程参与者说话状态
|
||||||
remoteParticipants.value.forEach((data, identity) => {
|
remoteParticipants.value.forEach((data, identity) => {
|
||||||
const isSpeaking = speakers.some(speaker => speaker.identity === identity);
|
const isSpeaking = speakers.some(speaker => speaker.identity === identity);
|
||||||
if (data.isSpeaking !== isSpeaking) {
|
if (data.isSpeaking !== isSpeaking) {
|
||||||
@@ -448,7 +503,7 @@ function handleDataReceived(payload, participant, kind) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleConnectionStateChanged(state) {
|
function handleConnectionStateChanged(state) {
|
||||||
|
console.log('连接状态改变:', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新屏幕共享状态
|
// 更新屏幕共享状态
|
||||||
@@ -483,9 +538,10 @@ function addRemoteParticipant(participant) {
|
|||||||
identity: participant.identity,
|
identity: participant.identity,
|
||||||
cameraTrack: null,
|
cameraTrack: null,
|
||||||
screenTrack: null,
|
screenTrack: null,
|
||||||
|
audioTrack: null,
|
||||||
hasCameraTrack: false,
|
hasCameraTrack: false,
|
||||||
hasScreenTrack: false,
|
hasScreenTrack: false,
|
||||||
audioEnabled: participant.isMicrophoneEnabled,
|
audioEnabled: false,
|
||||||
videoEnabled: participant.isCameraEnabled,
|
videoEnabled: participant.isCameraEnabled,
|
||||||
isSpeaking: false
|
isSpeaking: false
|
||||||
};
|
};
|
||||||
@@ -495,12 +551,14 @@ function addRemoteParticipant(participant) {
|
|||||||
|
|
||||||
function removeRemoteParticipant(participant) {
|
function removeRemoteParticipant(participant) {
|
||||||
if (remoteParticipants.value.has(participant.identity)) {
|
if (remoteParticipants.value.has(participant.identity)) {
|
||||||
// 清理视频元素
|
// 清理音视频元素
|
||||||
const identity = participant.identity;
|
const identity = participant.identity;
|
||||||
detachTrackFromParticipantVideo(identity, 'camera');
|
detachTrackFromParticipantVideo(identity, 'camera');
|
||||||
detachTrackFromParticipantVideo(identity, 'screen');
|
detachTrackFromParticipantVideo(identity, 'screen');
|
||||||
|
detachTrackFromParticipantAudio(identity);
|
||||||
remoteParticipants.value.delete(identity);
|
remoteParticipants.value.delete(identity);
|
||||||
videoElementsMap.value.delete(identity);
|
videoElementsMap.value.delete(identity);
|
||||||
|
audioElementsMap.value.delete(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,6 +589,23 @@ function removeParticipantTrack(participant, source) {
|
|||||||
remoteParticipants.value.set(participant.identity, { ...data });
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新参与者音频轨道信息
|
||||||
|
function updateParticipantAudioTrack(participant, track) {
|
||||||
|
const data = remoteParticipants.value.get(participant.identity);
|
||||||
|
if (!data) return;
|
||||||
|
data.audioTrack = track;
|
||||||
|
data.audioEnabled = true;
|
||||||
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeParticipantAudioTrack(participant) {
|
||||||
|
const data = remoteParticipants.value.get(participant.identity);
|
||||||
|
if (!data) return;
|
||||||
|
data.audioTrack = null;
|
||||||
|
data.audioEnabled = false;
|
||||||
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
|
}
|
||||||
|
|
||||||
// 附加轨道到视频元素
|
// 附加轨道到视频元素
|
||||||
function attachTrackToVideo(videoElement, track) {
|
function attachTrackToVideo(videoElement, track) {
|
||||||
if (!videoElement || !track) return;
|
if (!videoElement || !track) return;
|
||||||
@@ -544,6 +619,22 @@ function attachTrackToVideo(videoElement, track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 附加轨道到音频元素
|
||||||
|
function attachTrackToAudio(audioElement, track) {
|
||||||
|
if (!audioElement || !track) return;
|
||||||
|
try {
|
||||||
|
const mediaStream = new MediaStream();
|
||||||
|
mediaStream.addTrack(track.mediaStreamTrack);
|
||||||
|
audioElement.srcObject = mediaStream;
|
||||||
|
// 确保音频元素可以播放
|
||||||
|
audioElement.play().catch(error => {
|
||||||
|
console.warn('音频播放失败:', error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('附加轨道到音频元素失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 附加轨道到参与者的视频元素
|
// 附加轨道到参与者的视频元素
|
||||||
function attachTrackToParticipantVideo(identity, source, track) {
|
function attachTrackToParticipantVideo(identity, source, track) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -555,6 +646,14 @@ function attachTrackToParticipantVideo(identity, source, track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 附加轨道到参与者的音频元素
|
||||||
|
function attachTrackToParticipantAudio(identity, track) {
|
||||||
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
|
if (audioElement) {
|
||||||
|
attachTrackToAudio(audioElement, track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 从参与者的视频元素分离轨道
|
// 从参与者的视频元素分离轨道
|
||||||
function detachTrackFromParticipantVideo(identity, source) {
|
function detachTrackFromParticipantVideo(identity, source) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -566,12 +665,22 @@ function detachTrackFromParticipantVideo(identity, source) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从参与者的音频元素分离轨道
|
||||||
|
function detachTrackFromParticipantAudio(identity) {
|
||||||
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
|
if (audioElement && audioElement.srcObject) {
|
||||||
|
audioElement.srcObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateParticipantTracks(participant) {
|
function updateParticipantTracks(participant) {
|
||||||
const data = remoteParticipants.value.get(participant.identity);
|
const data = remoteParticipants.value.get(participant.identity);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
// 检查视频轨道状态
|
// 检查视频轨道状态
|
||||||
let hasCamera = false;
|
let hasCamera = false;
|
||||||
let hasScreen = false;
|
let hasScreen = false;
|
||||||
|
let hasAudio = false;
|
||||||
|
|
||||||
// 检查已发布的轨道
|
// 检查已发布的轨道
|
||||||
participant.videoTrackPublications.forEach(publication => {
|
participant.videoTrackPublications.forEach(publication => {
|
||||||
if (publication.isSubscribed && publication.track) {
|
if (publication.isSubscribed && publication.track) {
|
||||||
@@ -589,9 +698,20 @@ function updateParticipantTracks(participant) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 检查音频轨道
|
||||||
|
participant.audioTrackPublications.forEach(publication => {
|
||||||
|
if (publication.isSubscribed && publication.track) {
|
||||||
|
hasAudio = true;
|
||||||
|
if (!data.audioTrack) {
|
||||||
|
data.audioTrack = publication.track;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
data.hasCameraTrack = hasCamera;
|
data.hasCameraTrack = hasCamera;
|
||||||
data.hasScreenTrack = hasScreen;
|
data.hasScreenTrack = hasScreen;
|
||||||
data.audioEnabled = participant.isMicrophoneEnabled;
|
data.audioEnabled = hasAudio;
|
||||||
data.videoEnabled = participant.isCameraEnabled;
|
data.videoEnabled = participant.isCameraEnabled;
|
||||||
remoteParticipants.value.set(participant.identity, { ...data });
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
}
|
}
|
||||||
@@ -639,6 +759,18 @@ async function enableCamera() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enableMicrophone() {
|
||||||
|
try {
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('开启麦克风失败:', error);
|
||||||
|
microphoneEnabled.value = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleCamera() {
|
async function toggleCamera() {
|
||||||
try {
|
try {
|
||||||
if (cameraEnabled.value) {
|
if (cameraEnabled.value) {
|
||||||
@@ -709,9 +841,27 @@ async function toggleMicrophone() {
|
|||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
ElMessage.info('麦克风已关闭');
|
ElMessage.info('麦克风已关闭');
|
||||||
} else {
|
} else {
|
||||||
|
// 确保有音频输入设备权限
|
||||||
|
try {
|
||||||
|
// 先检查麦克风权限
|
||||||
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('无法访问麦克风,请检查权限设置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await room.localParticipant.setMicrophoneEnabled(true);
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
microphoneEnabled.value = true;
|
microphoneEnabled.value = true;
|
||||||
ElMessage.success('麦克风已开启');
|
ElMessage.success('麦克风已开启');
|
||||||
|
|
||||||
|
// 等待音频轨道发布
|
||||||
|
setTimeout(() => {
|
||||||
|
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
||||||
|
const audioPublication = audioPublications.find(pub => pub.track);
|
||||||
|
if (audioPublication && audioPublication.track) {
|
||||||
|
console.log('本地音频轨道已发布:', audioPublication.track);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换麦克风失败:', error);
|
console.error('切换麦克风失败:', error);
|
||||||
@@ -738,12 +888,7 @@ async function toggleScreenShare() {
|
|||||||
|
|
||||||
async function joinRoomBtn() {
|
async function joinRoomBtn() {
|
||||||
try {
|
try {
|
||||||
// if (!formModel.room.trim() || !formModel.uid.trim()) {
|
|
||||||
// ElMessage.error('请填写房间名称和用户编码');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
const res = await getRoomToken({max_participants: 20});
|
const res = await getRoomToken({max_participants: 20});
|
||||||
// const res = await getRoomToken(formModel);
|
|
||||||
const token = res.data.access_token;
|
const token = res.data.access_token;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('获取 token 失败');
|
throw new Error('获取 token 失败');
|
||||||
@@ -761,7 +906,6 @@ async function joinRoomBtn() {
|
|||||||
// 离开会议函数
|
// 离开会议函数
|
||||||
async function leaveRoom() {
|
async function leaveRoom() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const res = await exitRoomApi(room.name)
|
const res = await exitRoomApi(room.name)
|
||||||
console.log(res,'离开房间成功')
|
console.log(res,'离开房间成功')
|
||||||
// 停止屏幕共享(如果正在共享)
|
// 停止屏幕共享(如果正在共享)
|
||||||
@@ -796,12 +940,14 @@ function resetRoomState() {
|
|||||||
cameraEnabled.value = false;
|
cameraEnabled.value = false;
|
||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
isScreenSharing.value = false;
|
isScreenSharing.value = false;
|
||||||
|
isLocalSpeaking.value = false;
|
||||||
status.value = true;
|
status.value = true;
|
||||||
hostUid.value = '';
|
hostUid.value = '';
|
||||||
|
|
||||||
// 重置远程参与者
|
// 重置远程参与者
|
||||||
remoteParticipants.value.clear();
|
remoteParticipants.value.clear();
|
||||||
videoElementsMap.value.clear();
|
videoElementsMap.value.clear();
|
||||||
|
audioElementsMap.value.clear();
|
||||||
|
|
||||||
// 重置屏幕共享状态
|
// 重置屏幕共享状态
|
||||||
screenSharingUser.value = '';
|
screenSharingUser.value = '';
|
||||||
@@ -813,12 +959,18 @@ function resetRoomState() {
|
|||||||
localVideo.value.srcObject = null;
|
localVideo.value.srcObject = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理音频元素
|
||||||
|
if (localAudio.value && localAudio.value.srcObject) {
|
||||||
|
localAudio.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
|
localAudio.value.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (screenShareVideo.value && screenShareVideo.value.srcObject) {
|
if (screenShareVideo.value && screenShareVideo.value.srcObject) {
|
||||||
screenShareVideo.value.srcObject.getTracks().forEach(track => track.stop());
|
screenShareVideo.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
screenShareVideo.value.srcObject = null;
|
screenShareVideo.value.srcObject = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理所有远程参与者的视频元素
|
// 清理所有远程参与者的音视频元素
|
||||||
videoElementsMap.value.forEach((elements, identity) => {
|
videoElementsMap.value.forEach((elements, identity) => {
|
||||||
if (elements.camera && elements.camera.srcObject) {
|
if (elements.camera && elements.camera.srcObject) {
|
||||||
elements.camera.srcObject.getTracks().forEach(track => track.stop());
|
elements.camera.srcObject.getTracks().forEach(track => track.stop());
|
||||||
@@ -829,12 +981,15 @@ function resetRoomState() {
|
|||||||
elements.screen.srcObject = null;
|
elements.screen.srcObject = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
audioElementsMap.value.forEach((audioElement, identity) => {
|
||||||
|
if (audioElement && audioElement.srcObject) {
|
||||||
|
audioElement.srcObject.getTracks().forEach(track => track.stop());
|
||||||
|
audioElement.srcObject = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 在组件卸载时也清理资源
|
// 在组件卸载时也清理资源
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (room && room.state === 'connected') {
|
if (room && room.state === 'connected') {
|
||||||
@@ -843,6 +998,16 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// 确保在连接前请求音频权限
|
||||||
|
try {
|
||||||
|
// 预请求音频权限
|
||||||
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
console.log('音频权限已获取');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('音频权限获取失败:', error);
|
||||||
|
ElMessage.warning('请允许麦克风权限以使用音频功能');
|
||||||
|
}
|
||||||
|
|
||||||
if(route.query.type == '1'){
|
if(route.query.type == '1'){
|
||||||
await joinRoomBtn()
|
await joinRoomBtn()
|
||||||
hostUid.value = roomStore.userUid
|
hostUid.value = roomStore.userUid
|
||||||
@@ -860,13 +1025,41 @@ onMounted(async () => {
|
|||||||
await room.connect(wsURL, token, {
|
await room.connect(wsURL, token, {
|
||||||
autoSubscribe: true,
|
autoSubscribe: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* 添加音频状态指示器样式 */
|
||||||
|
.audio-indicator {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-indicator.muted {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-indicator:not(.muted) {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.speaking {
|
||||||
|
background-color: #67c23a;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏音频元素,不显示在页面上 */
|
||||||
|
.audio-element, .participant-audio {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
/* 全局样式 */
|
/* 全局样式 */
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
Reference in New Issue
Block a user