视频会议
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user