From 2568f15bda4174a5fe95462aa83f8395459f03d2 Mon Sep 17 00:00:00 2001 From: duoaohui <928970622@qq.com> Date: Sat, 23 May 2026 18:55:47 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E4=BC=9A=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/cloud-law/video-call.vue | 85 ++++++++++++++++++--- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/cloud-law/video-call.vue b/frontend/src/pages/cloud-law/video-call.vue index 50acd0d..4c971db 100644 --- a/frontend/src/pages/cloud-law/video-call.vue +++ b/frontend/src/pages/cloud-law/video-call.vue @@ -302,6 +302,8 @@ const startTimer = () => { } } +const localMediaError = ref('') + const joinMeeting = async () => { if (joining.value) return const code = formattedCode.value @@ -330,12 +332,13 @@ const joinMeeting = async () => { startAudio: true, startVideo: startWithVideo } - await meetingStore.joinAndEnter(code, '', { - startAudio: true, - startVideo: startWithVideo, - audioDeviceId: '', - videoDeviceId: '' - }) + + // 关键:和 preview.vue 一致,进会前先 getUserMedia 拿到 audio+video 的原始 track, + // 再把 track 直接交给 joinAndEnter 复用,避免 mediasoup 入会后再次 getUserMedia + // 在 iOS Safari/微信 WebView 上触发 NotReadableError / 权限拒绝。 + const prefs = await prepareLocalMediaTracks(startWithVideo) + + await meetingStore.joinAndEnter(code, '', prefs) startTimer() scheduleVideoRetry() } catch (err) { @@ -346,6 +349,53 @@ const joinMeeting = async () => { } } +/** + * 进会前同步申请本地音视频。与 preview.vue 同策略: + * 一次 getUserMedia 拿到 audio + video track 直接交给 mediasoup produce, + * 避免入会后再调 getUserMedia 在某些 WebView/Safari 里失败导致黑屏。 + */ +const prepareLocalMediaTracks = async (startWithVideo) => { + const prefs = { + startAudio: true, + startVideo: startWithVideo, + audioDeviceId: '', + videoDeviceId: '', + audioTrack: null, + videoTrack: null + } + // #ifdef H5 + if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + console.warn('[CloudLawVideoCall] navigator.mediaDevices 不可用,跳过预申请') + return prefs + } + try { + const constraints = { + audio: true, + video: startWithVideo + ? { width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 15, max: 24 }, facingMode: 'user' } + : false + } + const stream = await navigator.mediaDevices.getUserMedia(constraints) + prefs.audioTrack = stream.getAudioTracks()[0] || null + prefs.videoTrack = stream.getVideoTracks()[0] || null + // 如果用户拒了视频,stream 里不会有 video track,自动降级为不开摄像头 + if (startWithVideo && !prefs.videoTrack) { + prefs.startVideo = false + } + if (!prefs.audioTrack) { + prefs.startAudio = false + } + } catch (err) { + console.error('[CloudLawVideoCall] prepare media failed', err) + localMediaError.value = `预申请音视频失败:${err?.name || ''} ${err?.message || err}` + // 拿不到也继续走 joinAndEnter,让后端协议建好;用户可以在入会后手动点麦/摄像头按钮重试 + prefs.startAudio = false + prefs.startVideo = false + } + // #endif + return prefs +} + const scheduleVideoRetry = () => { if (isVoiceMode.value || videoRetryHandle || videoEnabled.value) return videoRetryHandle = setTimeout(async () => { @@ -354,7 +404,16 @@ const scheduleVideoRetry = () => { try { await meetingStore.startLocalVideo() } catch (err) { - console.warn('[CloudLawVideoCall] retry start video failed', err) + console.error('[CloudLawVideoCall] retry start video failed', err) + localMediaError.value = `自动开摄像头失败:${err?.message || err}` + } + if (!meetingStore.localAudioEnabled) { + try { + await meetingStore.startLocalAudio() + } catch (err) { + console.error('[CloudLawVideoCall] retry start audio failed', err) + localMediaError.value = `${localMediaError.value || ''} | 自动开麦克风失败:${err?.message || err}` + } } }, 800) } @@ -367,9 +426,13 @@ const onMicToggle = async () => { await meetingStore.stopLocalAudio() } else { await meetingStore.startLocalAudio() + localMediaError.value = '' } } catch (err) { - uni.showToast({ title: err?.message || '麦克风操作失败', icon: 'none' }) + console.error('[CloudLawVideoCall] mic toggle failed', err) + const msg = err?.message || '麦克风操作失败' + localMediaError.value = `麦克风:${msg}` + uni.showModal({ title: '麦克风开启失败', content: msg, showCancel: false }) } finally { audioLoading.value = false } @@ -383,9 +446,13 @@ const onCamToggle = async () => { await meetingStore.stopLocalVideo() } else { await meetingStore.startLocalVideo() + localMediaError.value = '' } } catch (err) { - uni.showToast({ title: err?.message || '摄像头操作失败', icon: 'none' }) + console.error('[CloudLawVideoCall] cam toggle failed', err) + const msg = err?.message || '摄像头操作失败' + localMediaError.value = `摄像头:${msg}` + uni.showModal({ title: '摄像头开启失败', content: msg, showCancel: false }) } finally { videoLoading.value = false }