PureJS版Zoom Meeting SDK:iOS Safari中主持人自身视频不可见问题
iOS Safari下Zoom Meeting SDK主持人视频不可见问题
问题现象
基于PureJS集成Zoom Meeting SDK开发的网站,在Chrome、Firefox浏览器中运行正常,但在iOS Safari环境下,主持人无法看到自身视频。通过元素检查发现,对应的视频元素top和left值被设置为-10000,导致视频被移出可视区域。该场景仅涉及主持人与一名参与者,且已使用完整打包的SDK。
参考了Zoom官方Web示例中的/src/js/meeting/session/video/video-toggler.js代码,相关逻辑如下:
import state from "../simple-state"; import { VideoQuality } from '@zoom/videosdk'; import { VIDEO_CANVAS, VIDEO_CANVAS_DIMS, SELF_VIDEO_ELEMENT, SELF_VIDEO_CANVAS } from './video-render-props'; let prevIsSelfVideoOn = false; let prevIsParticipantVideoOn = false; export const toggleSelfVideo = async (mediaStream, isVideoOn) => { const isUsingVideoElementToStartVideo = typeof window.OffscreenCanvas === 'function' && !mediaStream.isSupportMultipleVideos(); const isRenderingSingleVideoOnCanvas = typeof window.OffscreenCanvas !== 'function' && !mediaStream.isSupportMultipleVideos(); if (typeof isVideoOn !== 'boolean' || prevIsSelfVideoOn === isVideoOn) { return; } const canvas = isRenderingSingleVideoOnCanvas ? SELF_VIDEO_CANVAS : VIDEO_CANVAS; if (isVideoOn) { if (isUsingVideoElementToStartVideo) { SELF_VIDEO_ELEMENT.style.display = 'block'; SELF_VIDEO_ELEMENT.style.width = '50%'; SELF_VIDEO_ELEMENT.style.left = '50%'; await mediaStream.startVideo({ videoElement: SELF_VIDEO_ELEMENT }); } else { await mediaStream.startVideo(); if (isRenderingSingleVideoOnCanvas) { SELF_VIDEO_CANVAS.style.display = 'block'; SELF_VIDEO_CANVAS.style.width = '50%'; SELF_VIDEO_CANVAS.style.height = '50%'; SELF_VIDEO_CANVAS.style.left = '50%'; SELF_VIDEO_CANVAS.style.top = '50%'; SELF_VIDEO_CANVAS.style.transform = 'translateY(-50%)'; await mediaStream.renderVideo( canvas, state.selfId, VIDEO_CANVAS_DIMS.Width / 2, VIDEO_CANVAS_DIMS.Height / 2, 0, 0, VideoQuality.Video_360P ); } else { await mediaStream.renderVideo( canvas, state.selfId, VIDEO_CANVAS_DIMS.Width / 2, VIDEO_CANVAS_DIMS.Height, VIDEO_CANVAS_DIMS.Width / 2, 0, VideoQuality.Video_360P ); } } } else { await mediaStream.stopVideo(); if (!isUsingVideoElementToStartVideo) { if (isRenderingSingleVideoOnCanvas) { SELF_VIDEO_CANVAS.style.display = 'none'; } await mediaStream.stopRenderVideo(canvas, state.selfId); } else { SELF_VIDEO_ELEMENT.style.display = 'none'; } } prevIsSelfVideoOn = isVideoOn; }; export const toggleParticipantVideo = async (mediaStream, userId, isVideoOn) => { if (typeof isVideoOn !== 'boolean' || prevIsParticipantVideoOn === isVideoOn) { return; } if (isVideoOn) { await mediaStream.renderVideo( VIDEO_CANVAS, userId, VIDEO_CANVAS_DIMS.Width / 2, VIDEO_CANVAS_DIMS.Height, 0, 0, VideoQuality.Video_360P ); } else { await mediaStream.stopRenderVideo(VIDEO_CANVAS, userId); } prevIsParticipantVideoOn = isVideoOn; };
问题分析与修复方案
核心原因
iOS Safari对OffscreenCanvas的支持逻辑与桌面浏览器存在差异,导致代码中isUsingVideoElementToStartVideo的判断出现偏差。Zoom SDK内部会将视频元素偏移到可视区域外(设置top/left: -10000),而现有代码未正确覆盖该默认样式。
修复步骤
- 强制覆盖视频元素偏移样式:在调用
startVideo后,手动重置视频元素的定位属性,确保其处于可视区域内:
await mediaStream.startVideo({ videoElement: SELF_VIDEO_ELEMENT }); // 覆盖SDK设置的隐藏偏移 SELF_VIDEO_ELEMENT.style.top = 'auto'; SELF_VIDEO_ELEMENT.style.left = '50%'; SELF_VIDEO_ELEMENT.style.transform = 'translateX(-50%)';
- 修正
OffscreenCanvas判断逻辑:针对iOS Safari单独适配,调整视频渲染方式的判断条件:
// 新增iOS设备判断 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; // 修改原判断逻辑,优先为iOS使用video元素渲染 const isUsingVideoElementToStartVideo = (isIOS || typeof window.OffscreenCanvas !== 'function') && !mediaStream.isSupportMultipleVideos();
- 设置视频元素初始样式:在页面初始化时,给
SELF_VIDEO_ELEMENT添加基础定位样式,避免SDK修改后无法恢复:
#self-video-element { position: absolute; display: none; top: auto; left: 50%; transform: translateX(-50%); width: 50%; }
完整修改后的toggleSelfVideo函数
import state from "../simple-state"; import { VideoQuality } from '@zoom/videosdk'; import { VIDEO_CANVAS, VIDEO_CANVAS_DIMS, SELF_VIDEO_ELEMENT, SELF_VIDEO_CANVAS } from './video-render-props'; let prevIsSelfVideoOn = false; let prevIsParticipantVideoOn = false; export const toggleSelfVideo = async (mediaStream, isVideoOn) => { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const isUsingVideoElementToStartVideo = (isIOS || typeof window.OffscreenCanvas !== 'function') && !mediaStream.isSupportMultipleVideos(); const isRenderingSingleVideoOnCanvas = typeof window.OffscreenCanvas !== 'function' && !mediaStream.isSupportMultipleVideos(); if (typeof isVideoOn !== 'boolean' || prevIsSelfVideoOn === isVideoOn) { return; } const canvas = isRenderingSingleVideoOnCanvas ? SELF_VIDEO_CANVAS : VIDEO_CANVAS; if (isVideoOn) { if (isUsingVideoElementToStartVideo) { SELF_VIDEO_ELEMENT.style.display = 'block'; SELF_VIDEO_ELEMENT.style.width = '50%'; SELF_VIDEO_ELEMENT.style.left = '50%'; SELF_VIDEO_ELEMENT.style.top = 'auto'; SELF_VIDEO_ELEMENT.style.transform = 'translateX(-50%)'; await mediaStream.startVideo({ videoElement: SELF_VIDEO_ELEMENT }); // 再次强制覆盖SDK可能设置的偏移 SELF_VIDEO_ELEMENT.style.top = 'auto'; SELF_VIDEO_ELEMENT.style.left = '50%'; } else { await mediaStream.startVideo(); if (isRenderingSingleVideoOnCanvas) { SELF_VIDEO_CANVAS.style.display = 'block'; SELF_VIDEO_CANVAS.style.width = '50%'; SELF_VIDEO_CANVAS.style.height = '50%'; SELF_VIDEO_CANVAS.style.left = '50%'; SELF_VIDEO_CANVAS.style.top = '50%'; SELF_VIDEO_CANVAS.style.transform = 'translateY(-50%)'; await mediaStream.renderVideo( canvas, state.selfId, VIDEO_CANVAS_DIMS.Width / 2, VIDEO_CANVAS_DIMS.Height / 2, 0, 0, VideoQuality.Video_360P ); } else { await mediaStream.renderVideo( canvas, state.selfId, VIDEO_CANVAS_DIMS.Width / 2, VIDEO_CANVAS_DIMS.Height, VIDEO_CANVAS_DIMS.Width / 2, 0, VideoQuality.Video_360P ); } } } else { await mediaStream.stopVideo(); if (!isUsingVideoElementToStartVideo) { if (isRenderingSingleVideoOnCanvas) { SELF_VIDEO_CANVAS.style.display = 'none'; } await mediaStream.stopRenderVideo(canvas, state.selfId); } else { SELF_VIDEO_ELEMENT.style.display = 'none'; } } prevIsSelfVideoOn = isVideoOn; }; // toggleParticipantVideo函数保持不变 export const toggleParticipantVideo = async (mediaStream, userId, isVideoOn) => { if (typeof isVideoOn !== 'boolean' || prevIsParticipantVideoOn === isVideoOn) { return; } if (isVideoOn) { await mediaStream.renderVideo( VIDEO_CANVAS, userId, VIDEO_CANVAS_DIMS.Width / 2, VIDEO_CANVAS_DIMS.Height, 0, 0, VideoQuality.Video_360P ); } else { await mediaStream.stopRenderVideo(VIDEO_CANVAS, userId); } prevIsParticipantVideoOn = isVideoOn; };
内容的提问来源于stack exchange,提问作者Rahul Gupta
相关产品推荐
相关产品推荐

