视频会议

This commit is contained in:
duoaohui
2026-05-23 18:55:47 +08:00
parent 39594388e3
commit 2568f15bda

View File

@@ -302,6 +302,8 @@ const startTimer = () => {
} }
} }
const localMediaError = ref('')
const joinMeeting = async () => { const joinMeeting = async () => {
if (joining.value) return if (joining.value) return
const code = formattedCode.value const code = formattedCode.value
@@ -330,12 +332,13 @@ const joinMeeting = async () => {
startAudio: true, startAudio: true,
startVideo: startWithVideo startVideo: startWithVideo
} }
await meetingStore.joinAndEnter(code, '', {
startAudio: true, // 关键:和 preview.vue 一致,进会前先 getUserMedia 拿到 audio+video 的原始 track
startVideo: startWithVideo, // 再把 track 直接交给 joinAndEnter 复用,避免 mediasoup 入会后再次 getUserMedia
audioDeviceId: '', // 在 iOS Safari/微信 WebView 上触发 NotReadableError / 权限拒绝。
videoDeviceId: '' const prefs = await prepareLocalMediaTracks(startWithVideo)
})
await meetingStore.joinAndEnter(code, '', prefs)
startTimer() startTimer()
scheduleVideoRetry() scheduleVideoRetry()
} catch (err) { } 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 = () => { const scheduleVideoRetry = () => {
if (isVoiceMode.value || videoRetryHandle || videoEnabled.value) return if (isVoiceMode.value || videoRetryHandle || videoEnabled.value) return
videoRetryHandle = setTimeout(async () => { videoRetryHandle = setTimeout(async () => {
@@ -354,7 +404,16 @@ const scheduleVideoRetry = () => {
try { try {
await meetingStore.startLocalVideo() await meetingStore.startLocalVideo()
} catch (err) { } 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) }, 800)
} }
@@ -367,9 +426,13 @@ const onMicToggle = async () => {
await meetingStore.stopLocalAudio() await meetingStore.stopLocalAudio()
} else { } else {
await meetingStore.startLocalAudio() await meetingStore.startLocalAudio()
localMediaError.value = ''
} }
} catch (err) { } 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 { } finally {
audioLoading.value = false audioLoading.value = false
} }
@@ -383,9 +446,13 @@ const onCamToggle = async () => {
await meetingStore.stopLocalVideo() await meetingStore.stopLocalVideo()
} else { } else {
await meetingStore.startLocalVideo() await meetingStore.startLocalVideo()
localMediaError.value = ''
} }
} catch (err) { } 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 { } finally {
videoLoading.value = false videoLoading.value = false
} }