Subchapter 149.17
troubleshooting/common-issues.mdMarkdown11 KBView on GitHub
When something isn’t working, run through this checklist:
createClient() → init() → join() → getMediaStream()?getMediaStream() AFTER join() completed?peer-video-state-change?attachVideo() (not deprecated renderVideo())?Symptom: client.getMediaStream() returns undefined or null
Cause: Called before join() completed
Solution:
// WRONG
const stream = client.getMediaStream(); // undefined!
await client.join(...);
// CORRECT
await client.join(...);
const stream = client.getMediaStream(); // Works!Symptom: Video element created but shows black/nothing
Causes:
peer-video-state-change eventrenderVideo() instead of attachVideo()Solution:
// 1. Use attachVideo(), not renderVideo()
const videoElement = await stream.attachVideo(userId, VideoQuality.Video_360P);
// 2. Append to DOM
container.appendChild(videoElement);
// 3. Listen for events
client.on('peer-video-state-change', async (payload) => {
if (payload.action === 'Start') {
const element = await stream.attachVideo(payload.userId, VideoQuality.Video_360P);
container.appendChild(element);
} else {
await stream.detachVideo(payload.userId);
}
});Symptom: Join mid-session, only your video shows, not others’
Cause: Existing participants’ videos don’t auto-render
Solution:
// After joining, manually render existing participants
async function renderExistingParticipants() {
await new Promise(resolve => setTimeout(resolve, 500));
const users = client.getAllUser();
const currentUserId = client.getCurrentUserInfo().userId;
for (const user of users) {
if (user.bVideoOn && user.userId !== currentUserId) {
const element = await stream.attachVideo(user.userId, VideoQuality.Video_360P);
document.getElementById(`video-${user.userId}`).appendChild(element);
}
}
}Symptom: SDK global not available
Causes:
source.zoom.us CDNSolutions:
Solution 1 - Use a permitted fallback copy:
# If your environment blocks `source.zoom.us`, you can mirror/self-host as a fallback
# only if permitted and you can keep versions in sync with the SDK you target.
curl "https://source.zoom.us/videosdk/zoom-video-2.3.12.min.js" -o public/js/zoom-video-sdk.min.js<script src="js/zoom-video-sdk.min.js"></script>Solution 2 - Wait for SDK to load:
function waitForSDK(timeout = 10000) {
return new Promise((resolve, reject) => {
if (typeof WebVideoSDK !== 'undefined') {
resolve();
return;
}
const start = Date.now();
const check = setInterval(() => {
if (typeof WebVideoSDK !== 'undefined') {
clearInterval(check);
resolve();
} else if (Date.now() - start > timeout) {
clearInterval(check);
reject(new Error('SDK failed to load'));
}
}, 100);
});
}
await waitForSDK();
const ZoomVideo = WebVideoSDK.default;Symptom: ZoomVideo.createClient() fails with CDN
Cause: CDN exports as WebVideoSDK, not ZoomVideo
Solution:
// NPM
import ZoomVideo from '@zoom/videosdk';
// CDN
const ZoomVideo = WebVideoSDK.default; // Note: .default!
const client = ZoomVideo.createClient();Symptom: join() throws error about invalid signature
Causes:
exp claim)tpc claimSolution:
tpc valueSymptom: startVideo() or startAudio() fails
Cause: Browser permission denied
Solution:
// Check permissions before starting
try {
await stream.startVideo();
} catch (error) {
if (error.type === 'INSUFFICIENT_PRIVILEGES') {
// Permission denied - guide user
alert('Please allow camera access in browser settings');
}
}Symptom: Video quality stays at 360p despite { hd: true }
Causes:
Solution:
// Check HD support
if (stream.isSupportHDVideo()) {
await stream.startVideo({ hd: true });
} else {
console.warn('HD not supported');
await stream.startVideo();
}
// Check SharedArrayBuffer
const sabAvailable = typeof SharedArrayBuffer === 'function';
if (!sabAvailable) {
console.warn('SharedArrayBuffer not available - add COOP/COEP headers');
}Server Headers for SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpSymptom: startShareScreen() fails or shows nothing
Cause: Using wrong element type (video vs canvas)
Solution:
// Check which element type to use
if (stream.isStartShareScreenWithVideoElement()) {
const video = document.getElementById('share-video');
await stream.startShareScreen(video as unknown as HTMLCanvasElement);
} else {
const canvas = document.getElementById('share-canvas');
await stream.startShareScreen(canvas);
}Symptom: Console shows CORS errors to Zoom telemetry
Cause: COOP/COEP headers blocking telemetry
Impact: None - harmless. SDK works fine.
Solution: Ignore these errors. They’re telemetry-related and don’t affect functionality.
| Error Type | Meaning | Common Cause |
|---|---|---|
INVALID_OPERATION | Duplicated operation | Calling same method twice |
INTERNAL_ERROR | Service unavailable | Network issues |
OPERATION_TIMEOUT | Timed out | Slow connection |
INSUFFICIENT_PRIVILEGES | Need host/manager | Not authorized |
IMPROPER_MEETING_STATE | Not in meeting | Wrong lifecycle stage |
INVALID_PARAMETERS | Wrong params | Bad user ID, etc. |
OPERATION_LOCKED | Property locked | Feature disabled |
| Issue | Solution |
|---|---|
| Virtual background not supported | Use alternative (blur not available) |
| Screen sharing requires macOS 15+ | Use Chrome/Firefox |
| Some audio issues | Enable patchJsMedia: true |
| Issue | Solution |
|---|---|
| Virtual background requires 90+ | Update Firefox |
| Some WebRTC issues | Use Chrome if critical |
| Issue | Solution |
|---|---|
| Limited screen share | Use desktop for sharing |
| Performance issues | Lower video quality |
| Camera switching | Use MobileVideoFacingMode enum |
const loggerClient = client.getLoggerClient({
level: 'debug'
});// Log all events
['connection-change', 'user-added', 'user-removed', 'peer-video-state-change'].forEach(event => {
client.on(event, (payload) => {
console.log(`Event: ${event}`, payload);
});
});const users = client.getAllUser();
console.table(users.map(u => ({
userId: u.userId,
name: u.displayName,
videoOn: u.bVideoOn,
muted: u.muted,
audio: u.audio
})));console.log('Active camera:', stream.getActiveCamera());
console.log('Active mic:', stream.getActiveMicrophone());
console.log('Capturing video:', stream.isCapturingVideo());
console.log('Audio muted:', stream.isAudioMuted());
console.log('HD supported:', stream.isSupportHDVideo());
console.log('Max quality:', stream.getVideoMaxQuality());These came up in production-style waiting-room to main-session transfers.
Symptom: Session joins, but media pipeline is flaky or blank.
Cause: CSP blocks WebAssembly execution used by js_media.min.js.
Fix: Ensure CSP script-src includes:
'wasm-unsafe-eval' 'unsafe-eval'Also keep required Zoom domains in script-src and allow worker-src blob:.
Symptom: Customer reaches main session but does not see advisor video.
Likely causes:
bVideoOn is false)Fix pattern:
getAllUser() render passpeer-video-state-change, user-added, and user-updated.Symptom: Self video renders far down the page instead of in tile.
Cause: Container CSS/DOM mismatch for SDK inserted elements.
Fix:
video-player-container for SDK video mounts.video-player-container video-player,
video-player-container canvas,
video-player-container video {
width: 100%;
height: 100%;
display: block;
}Symptom: Admit clicked, but customer does not transfer.
Cause: Command channel does not replay history. If customer wasn’t fully in waiting session yet, message is missed.
Fix:
Symptom: Console spam with CORS 531 errors.
Impact: Usually telemetry-only; does not block core session/media.
Action: Treat as noise unless accompanied by actual join or media API failures.
Nearby