You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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),而现有代码未正确覆盖该默认样式。

修复步骤

  1. 强制覆盖视频元素偏移样式:在调用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%)';
  1. 修正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();
  1. 设置视频元素初始样式:在页面初始化时,给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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 00:27:34