视频会议

This commit is contained in:
duoaohui
2026-05-23 14:15:19 +08:00
parent 2689301721
commit 2e66717932
5 changed files with 130 additions and 49 deletions

View File

@@ -15,7 +15,10 @@
入会成功后uni.redirectTo('/pages/meeting/room?code=xxx')
-->
<template>
<view class="page">
<view v-if="initializing || autoJoin" class="auto-join-page">
<text class="auto-join-title">正在进入会议</text>
</view>
<view v-else class="page">
<view class="container">
<text class="title">设备预览</text>
<text class="subtitle">{{ modeLabel }}确认设备和显示名称后加入会议</text>
@@ -112,6 +115,7 @@ const mode = ref('create')
const joinCode = ref('')
const joinPassword = ref('')
const autoJoin = ref(false)
const initializing = ref(true)
const modeLabel = computed(() => mode.value === 'create' ? '即将创建新会议' : `即将加入会议 ${joinCode.value}`)
@@ -359,17 +363,20 @@ const onCancel = () => {
uni.navigateBack()
}
const onJoin = async () => {
const onJoin = async (options = {}) => {
if (joining.value || !canJoin.value) return
joining.value = true
const skipMedia = options.skipMedia === true
const shouldStartAudio = !skipMedia && mediaPrefs.startAudio
const shouldStartVideo = !skipMedia && mediaPrefs.startVideo
meetingStore.devicePreview = {
audioDeviceId: selectedAudioId.value,
videoDeviceId: selectedVideoId.value,
speakerDeviceId: selectedSpeakerId.value,
displayName: displayName.value,
startAudio: mediaPrefs.startAudio,
startVideo: mediaPrefs.startVideo
startAudio: shouldStartAudio,
startVideo: shouldStartVideo
}
// iOS Safari/Chrome 上 preview 释放摄像头后 room 再次 getUserMedia 常触发 NotReadableError
@@ -380,21 +387,21 @@ const onJoin = async () => {
const rawAudioTrack = previewStream ? previewStream.getAudioTracks()[0] || null : null
const rawVideoTrack = previewStream ? previewStream.getVideoTracks()[0] || null : null
// 若用户关掉了"默认开麦/默认开摄像头",对应 track 不会被 mediasoup 接手,立即 stop 避免硬件泄漏
if (rawAudioTrack && !mediaPrefs.startAudio) {
if (rawAudioTrack && !shouldStartAudio) {
try { rawAudioTrack.stop() } catch {}
}
if (rawVideoTrack && !mediaPrefs.startVideo) {
if (rawVideoTrack && !shouldStartVideo) {
try { rawVideoTrack.stop() } catch {}
}
const audioTrack = mediaPrefs.startAudio ? rawAudioTrack : null
const videoTrack = mediaPrefs.startVideo ? rawVideoTrack : null
const audioTrack = shouldStartAudio ? rawAudioTrack : null
const videoTrack = shouldStartVideo ? rawVideoTrack : null
// previewStream 对象本身不再需要持有tracks 已取出交给 MediaEngine
// 避免在后续 onUnload 中误调 stopPreviewStream 把 track 给 stop 掉。
previewStream = null
const prefs = {
startAudio: mediaPrefs.startAudio,
startVideo: mediaPrefs.startVideo,
startAudio: shouldStartAudio,
startVideo: shouldStartVideo,
audioDeviceId: selectedAudioId.value,
videoDeviceId: selectedVideoId.value,
audioTrack,
@@ -429,44 +436,51 @@ const onJoin = async () => {
// ==================== 生命周期 ====================
onLoad(async (query) => {
mode.value = query?.mode === 'join' ? 'join' : 'create'
joinCode.value = query?.code || ''
autoJoin.value = query?.autoJoin === '1' || query?.autoJoin === 'true' || query?.autoJoin === true
console.log('[Preview] onLoad params:', {
query,
mode: mode.value,
joinCode: joinCode.value,
autoJoin: autoJoin.value,
userId: userStore.userInfo?.id || userStore.userInfo?.user_id || ''
})
// P0-4密码不再从 URL query 读取,改为从 meetingStore.draftJoinPayload 一次性消费
// 防止 URL 历史 / DevTools 留痕;读取后立即清空草稿避免二次写回
if (mode.value === 'join') {
const draft = meetingStore.draftJoinPayload
if (draft && (!joinCode.value || draft.code === joinCode.value)) {
joinPassword.value = draft.password || ''
meetingStore.draftJoinPayload = null
} else {
joinPassword.value = ''
}
}
// 先请求权限并预览,默认拿到第一个设备
await startPreview()
// 权限到手后枚举设备拿到 label
await loadDevices()
// 若此时已选中某个设备,重新启动预览确保走明确 deviceId避免浏览器默认设备切换
if (hasPermission.value && (selectedVideoId.value || selectedAudioId.value)) {
await startPreview()
}
if (autoJoin.value && canJoin.value) {
await nextTick()
console.log('[Preview] autoJoin start:', {
try {
mode.value = query?.mode === 'join' ? 'join' : 'create'
joinCode.value = query?.code || ''
autoJoin.value = query?.autoJoin === '1' || query?.autoJoin === 'true' || query?.autoJoin === true
console.log('[Preview] onLoad params:', {
query,
mode: mode.value,
joinCode: joinCode.value,
hasPermission: hasPermission.value,
permissionError: permissionError.value
autoJoin: autoJoin.value,
userId: userStore.userInfo?.id || userStore.userInfo?.user_id || ''
})
onJoin()
// P0-4密码不再从 URL query 读取,改为从 meetingStore.draftJoinPayload 一次性消费
// 防止 URL 历史 / DevTools 留痕;读取后立即清空草稿避免二次写回
if (mode.value === 'join') {
const draft = meetingStore.draftJoinPayload
if (draft && (!joinCode.value || draft.code === joinCode.value)) {
joinPassword.value = draft.password || ''
meetingStore.draftJoinPayload = null
} else {
joinPassword.value = ''
}
}
if (autoJoin.value && canJoin.value) {
await nextTick()
console.log('[Preview] autoJoin start:', {
joinCode: joinCode.value,
hasPermission: hasPermission.value,
permissionError: permissionError.value
})
onJoin({ skipMedia: true })
return
}
initializing.value = false
// 先请求权限并预览,默认拿到第一个设备
await startPreview()
// 权限到手后枚举设备拿到 label
await loadDevices()
// 若此时已选中某个设备,重新启动预览确保走明确 deviceId避免浏览器默认设备切换
if (hasPermission.value && (selectedVideoId.value || selectedAudioId.value)) {
await startPreview()
}
} catch (err) {
initializing.value = false
throw err
}
})
@@ -481,6 +495,17 @@ onBeforeUnmount(() => {
</script>
<style scoped>
.auto-join-page {
min-height: 100vh;
background: #F8FAFC;
display: flex;
align-items: center;
justify-content: center;
}
.auto-join-title {
font-size: 30rpx;
color: #475569;
}
.page { min-height: 100vh; background: #F8FAFC; padding: 24rpx; }
.container { max-width: 960rpx; margin: 0 auto; }
.title { font-size: 40rpx; font-weight: 600; color: #0F172A; display: block; margin: 24rpx 0 8rpx; }