Bot intermittently stuck in "joining" state after joining the call (with dev creds) via web SDK

Meeting SDK Type and Version

  • web SDK (CDN)
  • version 2.18.2

Description

  • at times bot is able to join the call, but gets stuck in the loading part
  • can see the loader moving forever
  • inside the zoom app → “connecting to audio”
  • issue happens randomly, did not happen 2 weeks before (using dev creds)
  • issue only occurs on the remote dev server (mumbai), locally it works fine, but it used to work fine in the remote server as well before.

@shashwatVar are there any errors in the console log?

please tag me in your response

@shashwatVar , We’ve seen this issue occur before if you’re using browser automation and clicking the “Join Audio” button too quickly after the bot joins the call. This can potentially trigger a race condition in the Meeting SDK’s join-meeting procedure, leading the bot to get stuck.

We’ve solved these issues at Recall.ai .
It’s a simple 3rd party API that lets you use meeting bots to get raw audio/video from meetings without you needing to spend months to build, scale and maintain these bots. Might be worth checking out.

Let me know if you have any questions!

@shashwatVar If you are using waiting room meetings, If you accept the bot too quickly (before sdk is able to load the waiting room screen) then sdk will get stuck at the joining screen or connecting audio screen.

Workaround is to accept slowly or reload the sdk when user is accepted in the meeting.

@chunsiong.zoom some insights into this:

  • this only happens with waiting rooms
  • no distinct different error logs
  • the only way to reproduce is to run the sdk on a system with limited resources and accept the bot from the waiting room immediately

@bharat.bhadresha thanks for the insights, they were accurate.
Wanted to understand how did you get this to work → by reloading the sdk what do we exactly mean ?

  • is it reloading the complete web page ?
  • is it calling a specific method from the sdk (ZoomMtg) ?
  • how do you programatically detect when to do the same ?

@chunsiong.zoom just in case the code would be required for understanding this better, here’s the meeting page js

window.addEventListener("DOMContentLoaded", handleDomContentLoaded);

let mediaCapturePermissionInterval;
let mediaCapturePermissionResponsed = false;

function handleDomContentLoaded(event) {
	console.log("DOM fully loaded and parsed");
	initializeWebSdk();
}

function initializeWebSdk() {
	const meetingSdkUtils = window.meetingSdkUtils;
	const meetingArgs = meetingSdkUtils.meeting.parseQuery();
	const meetingConfig = {
		sdkKey: meetingArgs.sdkKey,
		meetingNumber: meetingArgs.meetingNumber,
		userName: "Dive Assistant",
		passWord: meetingArgs.password,
		leaveUrl: "/index.html",
		role: parseInt(meetingArgs.role, 10),
		userEmail: meetingArgs.email || "",
		lang: "en-US",
		signature: meetingArgs.signature || "",
	};

	ZoomMtg.preLoadWasm();
	ZoomMtg.prepareJssdk();

	function beginJoin(signature) {
		ZoomMtg.init({
			leaveUrl: meetingConfig.leaveUrl,
			webEndpoint: meetingConfig.webEndpoint,
			disableCORP: !window.crossOriginIsolated,
			externalLinkPage: "./externalLinkPage.html",
			success: () => {
				logMessage("[zoomSdk-page] ZoomMtg.init Successful", "info", { meetingConfig, signature });
				ZoomMtg.i18n.load(meetingConfig.lang);
				ZoomMtg.i18n.reload(meetingConfig.lang);

				ZoomMtg.join({
					meetingNumber: meetingConfig.meetingNumber,
					userName: meetingConfig.userName,
					signature: meetingConfig.signature,
					sdkKey: meetingConfig.sdkKey,
					userEmail: meetingConfig.userEmail,
					passWord: meetingConfig.passWord,
					success: handleJoinSuccess,
					error: handleLeaveError,
				});
			},
			error: error => {
				logMessage("[zoomSdk-page] ZoomMtg.init Error", "error", { error });
			},
		});
	}

	beginJoin(meetingConfig.signature);
}

// ------------- Bot Helper functions ------------------//
function handleJoinSuccess(success) {
	logMessage("[zoomSdk-page] join Successful", "info", {
		success,
	});

	startMediaCapturePermissionTimer();
	setupMediaCaptureListeners();
}

function startMediaCapturePermissionTimer() {
	mediaCapturePermissionInterval = setInterval(() => {
		if (mediaCapturePermissionResponsed && mediaCapturePermissionInterval) {
			clearInterval(mediaCapturePermissionInterval);
		} else {
			requestMediaCapturePermission();
			console.log("pinging every 5 seconds");
		}
	}, 5000);
}

function requestMediaCapturePermission() {
	ZoomMtg.mediaCapturePermission({
		operate: "request",
	});
}

function handleMediaCapturePermissionSuccess(success) {
	logMessage("[zoomSdk-page] media capture permission success", "info", {
		success,
	});

	mediaCapturePermissionResponsed = true;
	if (success.allow) {
		startMediaCapture();
		logMessage("[zoomSdk-page] Media capture permission changed to ALLOW", "info");
	} else {
		stopMediaCapture();
		logMessage("[zoomSdk-page] Media capture permission changed to DENY", "warn");
		leaveMeetingAndHandleError();
	}
}

function handleMediaCapturePermissionError(error) {
	if (error.errorCode == "1") {
		logMessage("[zoomSdk-page] Media capture permission Active", "info", {
			error,
		});
		startMediaCapture();
	} else {
		logMessage("[zoomSdk-page] Media capture permission error", "error", {
			error,
		});
	}
}

function startMediaCapture() {
	ZoomMtg.mediaCapture({ record: "start" });
}

function stopMediaCapture() {
	ZoomMtg.mediaCapture({ record: "stop" });
}
function pauseMediaCapture() {
	ZoomMtg.mediaCapture({ record: "pause" });
}

function leaveMeetingAndHandleError() {
	ZoomMtg.leaveMeeting({
		success: handleLeaveSuccess,
		error: handleLeaveError,
	});
}

function handleLeaveSuccess(success) {
	logMessage("[zoomSdk-page] Bot has left the meeting", "info", {
		success,
	});

	window?.leaveZoomMtg("zoom_sdk_permission_rejected");
}

function handleLeaveError(error) {
	logMessage(
		"[zoomSdk-page] Bot failed to leave the meeting, use visibilityState of hidden to trigger leave",
		"error",
		{
			error,
		},
	);
	setupAccidentalLeaveListener(document, ZoomMtg);
}

function setupAccidentalLeaveListener(doc, zoom) {
	doc.addEventListener("visibilitychange", function () {
		if (doc.visibilityState === "hidden") {
			window?.leaveZoomMtg("everyone_left");
		}
	});
}

function setupMediaCaptureListeners() {
	ZoomMtg.inMeetingServiceListener("onMediaCapturePermissionChange", handleMediaCapturePermissionChange);
	ZoomMtg.inMeetingServiceListener("onMediaCaptureStatusChange", handleMediaCaptureStatusChange);
	ZoomMtg.inMeetingServiceListener("onUserLeave", handleUserLeave);
	ZoomMtg.inMeetingServiceListener("onMeetingStatus", handleMeetingStatusChange);
}

function handleMeetingStatusChange(data) {
	logMessage("[zoomSdk-page] onMeetingStatus -->", "info", {
		data,
	});

	if (data?.meetingStatus === 3) {
		window?.leaveZoomMtg("everyone_left");
	}
}

function handleMediaCapturePermissionChange({ allow: boolean }) {
	logMessage("[zoomSdk-page] onMediaCapturePermissionChange -->", "info", {
		boolean,
	});
	if (boolean) {
		startMediaCapture();
		logMessage("[zoomSdk-page] Media capture permission changed to ALLOW -->", "info", {
			boolean,
		});
	} else {
		logMessage("[zoomSdk-page] Media capture permission changed to DENY -->", "info", {
			boolean,
		});
		stopMediaCapture();
		leaveMeetingAndHandleError();
	}
}

function handleMediaCaptureStatusChange(data) {
	logMessage("[zoomSdk-page] onMediaCaptureStatusChange -->", "info", {
		data,
	});
	const { status, userId } = data;
	ZoomMtg.mute({ userId: userId, mute: true });
}

function handleUserLeave(data) {
	setupAccidentalLeaveListener(document, ZoomMtg);
}

function handleJoinError(error) {
	logMessage("[zoomSdk-page] join error", "error", {
		error,
	});
}

async function logMessage(message, type, obj = {}) {
	switch (type) {
		case "info": {
			await window?.loggerInfo(message, obj);
			break;
		}

		case "warn": {
			await window?.loggerWarn(message, obj);
			break;
		}

		case "error": {
			await window?.loggerError(message, obj);
			break;
		}

		default: {
			break;
		}
	}
}

This is mostly resolved in the latest release of SDK.

What we did was to check for attendee list after we receive meeting status event, if attendee list is not present then reload the whole page.