From 9df92977fda83a9b53618306ba75e834dc5b5137 Mon Sep 17 00:00:00 2001 From: duoaohui <928970622@qq.com> Date: Sat, 23 May 2026 21:35:46 +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/components/meeting/VideoTile.vue | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/meeting/VideoTile.vue b/frontend/src/components/meeting/VideoTile.vue index 818c328..b2a3ed5 100644 --- a/frontend/src/components/meeting/VideoTile.vue +++ b/frontend/src/components/meeting/VideoTile.vue @@ -117,49 +117,74 @@ const resolveDom = (refValue) => { return null } -const applyVideo = () => { +// 当前已经贴到 video / audio 元素上的 track,用于幂等判断,避免重复 setSrcObject 触发闪烁 +let boundVideoTrack = null +let boundAudioTrack = null + +const applyVideo = (retry = 0) => { // #ifdef H5 if (props.videoTrack && props.videoEnabled) { - const el = ensureMediaEl('video') - if (el) { - el.muted = props.isLocal - el.srcObject = new MediaStream([props.videoTrack]) - const playPromise = el.play && el.play() - if (playPromise && typeof playPromise.catch === 'function') { - playPromise.catch(() => {}) + const parent = resolveDom(mediaBox.value) + if (!parent) { + // DOM 尚未挂载,最多重试 10 次(≈500ms) + if (retry < 10) { + setTimeout(() => applyVideo(retry + 1), 50) } + return + } + const el = ensureMediaEl('video') + if (!el) return + el.muted = props.isLocal + if (boundVideoTrack !== props.videoTrack) { + el.srcObject = new MediaStream([props.videoTrack]) + boundVideoTrack = props.videoTrack + } + const playPromise = el.play && el.play() + if (playPromise && typeof playPromise.catch === 'function') { + playPromise.catch(() => {}) } } else { removeMediaEl('video') + boundVideoTrack = null } // #endif } -const applyAudio = () => { +const applyAudio = (retry = 0) => { // #ifdef H5 // 本地 tile 不挂载 audio(避免回声) if (!props.isLocal && props.audioTrack && props.audioEnabled) { - const el = ensureMediaEl('audio') - if (el) { - el.srcObject = new MediaStream([props.audioTrack]) - const playPromise = el.play && el.play() - if (playPromise && typeof playPromise.catch === 'function') { - playPromise.catch(() => {}) + const parent = resolveDom(mediaBox.value) + if (!parent) { + if (retry < 10) { + setTimeout(() => applyAudio(retry + 1), 50) } + return + } + const el = ensureMediaEl('audio') + if (!el) return + if (boundAudioTrack !== props.audioTrack) { + el.srcObject = new MediaStream([props.audioTrack]) + boundAudioTrack = props.audioTrack + } + const playPromise = el.play && el.play() + if (playPromise && typeof playPromise.catch === 'function') { + playPromise.catch(() => {}) } } else { removeMediaEl('audio') + boundAudioTrack = null } // #endif } -watch(() => [props.videoTrack, props.videoEnabled], () => nextTick(applyVideo), { immediate: true }) -watch(() => [props.audioTrack, props.audioEnabled, props.isLocal], () => nextTick(applyAudio), { immediate: true }) +watch(() => [props.videoTrack, props.videoEnabled], () => nextTick(() => applyVideo()), { immediate: true }) +watch(() => [props.audioTrack, props.audioEnabled, props.isLocal], () => nextTick(() => applyAudio()), { immediate: true }) -// 修复刚进入会议时画面不显示问题: -// watch 的 immediate:true 在 setup 阶段就触发 applyVideo,此时 mediaBox ref 尚未绑定真实 DOM; -// 即便包了 nextTick,在 uni-h5 偶发条件下仍可能命中 ref 未就绪,导致 srcObject 没贴上。 -// 这里在 onMounted 里再补一次绑定,确保 DOM ready 后必然挂上 track。 +// 修复刚进入会议时画面不显示、必须点切换才出图的问题: +// 1) watch 的 immediate:true 在 setup 阶段就触发 applyVideo,此时 mediaBox ref 尚未绑定真实 DOM; +// 2) 即便包了 nextTick,在 uni-h5 下 ref 偶发未就绪; +// 现在 applyVideo/applyAudio 在 DOM 未就绪时会自动重试,并在 onMounted 再强制兜底一次。 onMounted(() => { nextTick(() => { applyVideo()