We are using custom UI and when trying to video and audio on/off we are getting SDKERR_NO_PERMISSION. And this is only happening recording is on. The same is not reproducible in Zoom app or the sample provided. We’ve already checked for Audio and Camera manifest permission and that cannot be the case. It would help alot if we can know the list of reasons when this is thrown.
You are probably seeing this issue because camera permissions has not been granted. When you are using custom UI mode, you need to manually request permissions from the user at runtime. Declaring a permission in your app’s manifest is not the same as the user granting that permission. More information around how to properly request permissions can be found here.
Thanks for confirming. Since this is not reproducible in the SDK sample app, would you be able to send a demo app in which this issue is reproducible so that we can investigate further?
Just wanted to jump in here and say I’ve seen the same behavior, although I haven’t dug into it too much yet. I am also using a custom UI.
From what I remember, I joined a meeting that someone started recording. As soon as the recording started, I lost the ability to unmute and start my video. My buttons for doing each grayed out in my UI, which means InMeetingAudioController.canUnmuteMyAudio() and InMeetingVideoController.canUnmuteMyVideo() must have been returning false. It was not a permission problem. In fact, when I saw this happen I was on a Lollipop device, so permissions are granted at install time.
But now as I try to recreate it, I can’t. When the recording starts, I get a popup informing me about the recording. This is also odd considering I have a custom UI, why is the SDK popping up a dialog? But that’s a separate issue.
I can’t explain it, I know I saw this issue in the past but it’s no longer happening. I tried local recordings and cloud recordings. I tried downgrading the SDK. Still can’t reproduce this. When this originally happened for me, the popup dialog did not display, which let me to believe there was some runtime prompt I should be showing and wasn’t. But now the SDK appears to be showing the dialog for me, which might explain why the issue no longer happens?? I will keep trying.
We are working on sample project but there is a very slim chance that we will be reproduce the same issue. In the meantime, please see the below class. This is how all the zoom related code we have in our project.
interface ClassroomSdkDelegate {
fun init(zoomLink: String, uniqueId: Long)
val zoomSDK: ZoomSDK
var uniqueRecordId: Long
val initStateLiveData: LiveData<Event<ClassroomInitState>>
val actionLeaveMeetingLiveData: LiveData<Event<ActionLeaveMeeting>>
val actionMeetingEndByHostLiveData: LiveData<Event<ActionMeetingEndByHost>>
val actionDismissReactionLiveData: LiveData<Event<ActionDismissReactionDialog>>
val actionMsgReceivedLiveData: LiveData<Event<ActionMsgReceived>>
val actionActiveVideoLiveData: LiveData<ActionActiveVideoUpdated>
val actionVideoToggleDisabledLiveData: LiveData<Event<Unit>>
var hostId: Long
val actionMicLiveData: LiveData<Boolean>
fun toggleMic(isMicOn: Boolean)
val actionVideoLiveData: LiveData<Boolean>
fun toggleVideo(isVideoOn: Boolean)
val actionRaiseHandLiveData: LiveData<Boolean>
fun toggleRaiseHand(isHandRaised: Boolean)
fun reactEmoji(emojiString: String, emoticon: SDKEmojiReactionType)
fun sendMessage(message: String, toHostOnly: Boolean)
fun isOtherUserSharing(): Boolean
fun leaveMeeting()
}
class ClassroomSdkDelegateImpl @Inject constructor(
@ApplicationContext private val context: Context,
private val bbrAnalytics: BbrAnalytics,
) : ClassroomSdkDelegate, ZoomSDKInitializeListener, MeetingServiceListener,
InMeetingServiceListener, InMeetingShareController.InMeetingShareListener {
override val zoomSDK: ZoomSDK = ZoomSDK.getInstance()
private var zoomJoiningUrl: String = ""
override var uniqueRecordId: Long = 0L
override var hostId: Long = 0L
private val _initStateLiveData = MutableLiveData<Event<ClassroomInitState>>()
override val initStateLiveData = _initStateLiveData.toLiveData()
private val _actionActiveVideoLiveData = MutableLiveData<ActionActiveVideoUpdated>()
override val actionActiveVideoLiveData = _actionActiveVideoLiveData.toLiveData()
private val _actionLeaveMeetingLiveData = MutableLiveData<Event<ActionLeaveMeeting>>()
override val actionLeaveMeetingLiveData = _actionLeaveMeetingLiveData.toLiveData()
private val _actionMeetingEndByHostLiveData = MutableLiveData<Event<ActionMeetingEndByHost>>()
override val actionMeetingEndByHostLiveData = _actionMeetingEndByHostLiveData.toLiveData()
private val _actionDismissReactionLiveData =
MutableLiveData<Event<ActionDismissReactionDialog>>()
override val actionDismissReactionLiveData = _actionDismissReactionLiveData.toLiveData()
private val _actionMsgReceivedLiveData = MutableLiveData<Event<ActionMsgReceived>>()
override val actionMsgReceivedLiveData = _actionMsgReceivedLiveData.toLiveData()
private val _actionMicLiveData = MutableLiveData<Boolean>()
override val actionMicLiveData = _actionMicLiveData.toLiveData()
private val _actionVideoLiveData = MutableLiveData<Boolean>()
override val actionVideoLiveData = _actionVideoLiveData.toLiveData()
private val _actionRaiseHandLiveData = MutableLiveData(false)
override val actionRaiseHandLiveData = _actionRaiseHandLiveData.toLiveData()
private val _actionVideoToggleDisabledLiveData = MutableLiveData<Event<Unit>>()
override val actionVideoToggleDisabledLiveData = _actionVideoToggleDisabledLiveData.toLiveData()
override fun init(zoomLink: String, uniqueId: Long) {
zoomJoiningUrl = zoomLink
uniqueRecordId = uniqueId
when {
!zoomSDK.isInitialized -> {
zoomSDK.initialize(context, this, ZoomSDKInitParams().also {
it.appKey = context.getString(R.string.zoom_app_key)
it.appSecret = context.getString(R.string.zoom_app_secret)
})
}
else -> {
Timber.d("Zoom already isInitialized")
zoomSDK.meetingService.handZoomWebUrl(zoomJoiningUrl)
zoomMeetingSetup()
}
}
}
override fun onZoomSDKInitializeResult(errorCode: Int, internalErrorCode: Int) {
if (errorCode == ZoomError.ZOOM_ERROR_SUCCESS) {
Timber.i("SDK Success")
zoomMeetingSetup()
zoomSDK.meetingService.handZoomWebUrl(zoomJoiningUrl)
} else {
Timber.e(errorCode.toString())
Timber.e(internalErrorCode.toString())
Timber.e("SDK Fail")
}
}
override fun onZoomAuthIdentityExpired() {
Timber.e("onZoomAuthIdentityExpired() called")
}
private fun zoomMeetingSetup() {
zoomSDK.meetingSettingsHelper.isCustomizedMeetingUIEnabled = true
zoomSDK.meetingSettingsHelper.setAutoConnectVoIPWhenJoinMeeting(true)
zoomSDK.meetingSettingsHelper.setMuteMyMicrophoneWhenJoinMeeting(true)
zoomSDK.meetingSettingsHelper.disableShowMeetingNotification(true)
zoomSDK.meetingSettingsHelper.isGalleryVideoViewDisabled = true
zoomSDK.meetingSettingsHelper.setCustomizedNotificationData(
CustomizedNotificationData(
R.string.classroom_connect_meeting_notification_title,
R.string.classroom_connect_meeting_notification_text,
R.drawable.ic_trivia_bbr_logo, R.drawable.ic_trivia_bbr_logo,
R.color.deep_purple_500, R.drawable.ic_trivia_bbr_logo
)
) { _, _ -> true }
zoomSDK.meetingService.addListener(this)
zoomSDK.inMeetingService.addListener(this)
zoomSDK.inMeetingService.inMeetingShareController.addListener(this)
}
override fun onMeetingStatusChanged(
meetingStatus: MeetingStatus?, errorCode: Int, internalErrorCode: Int
) {
Timber.d(meetingStatus.toString())
when (meetingStatus) {
MeetingStatus.MEETING_STATUS_CONNECTING -> _initStateLiveData.postValue(
ClassroomInitState.ClassroomConnecting.toEvent()
)
MeetingStatus.MEETING_STATUS_WAITINGFORHOST -> _initStateLiveData.postValue(
ClassroomInitState.ClassroomWaitingForHost.toEvent()
)
MeetingStatus.MEETING_STATUS_INMEETING -> {
_initStateLiveData.postValue(ClassroomInitState.ClassroomInMeeting.toEvent())
if (hostId == 0L) zoomSDK.inMeetingService.inMeetingUserList.forEach {
if (zoomSDK.inMeetingService.isHostUser(it)) hostId = it
}
_actionActiveVideoLiveData.postValue(ActionActiveVideoUpdated(hostId))
}
MeetingStatus.MEETING_STATUS_RECONNECTING -> _initStateLiveData.postValue(
ClassroomInitState.ClassroomReconnecting.toEvent()
)
MeetingStatus.MEETING_STATUS_FAILED -> _initStateLiveData.postValue(
ClassroomInitState.ClassroomStateFailed.toEvent()
)
MeetingStatus.MEETING_STATUS_IN_WAITING_ROOM -> _initStateLiveData.postValue(
ClassroomInitState.ClassroomInWaitingRoom.toEvent()
)
MeetingStatus.MEETING_STATUS_DISCONNECTING ->
_actionMeetingEndByHostLiveData.postEvent(ActionMeetingEndByHost)
else -> Unit
}
}
override fun onHostAskUnMute(userId: Long) {
if (zoomSDK.inMeetingService.isMyself(userId)) toggleMic(false)
}
override fun onHostAskStartVideo(userId: Long) = toggleVideo(false)
override fun onMeetingFail(errorCode: Int, internalErrorCode: Int) {
Timber.d("Meeting Failed - errorCode:$errorCode internalErrorCode:$internalErrorCode")
bbrAnalytics.logEvent(
"Meeting Failed",
"errorCode" to errorCode, "internalErrorCode" to internalErrorCode
)
}
override fun onMeetingLeaveComplete(reason: Long) {
if (reason != 0L) {
_actionMeetingEndByHostLiveData.postEvent(ActionMeetingEndByHost)
}
}
override fun onUserAudioStatusChanged(userId: Long, p1: AudioStatus?) {
if (zoomSDK.inMeetingService.isMyself(userId))
_actionMicLiveData.postValue(zoomSDK.inMeetingService.inMeetingAudioController.isMyAudioMuted.not())
}
override fun onUserVideoStatusChanged(userId: Long, status: VideoStatus?) {
if (zoomSDK.inMeetingService.isMyself(userId))
_actionVideoLiveData.postValue(zoomSDK.inMeetingService.inMeetingVideoController.isMyVideoMuted.not())
}
override fun onChatMessageReceived(message: InMeetingChatMessage) {
_actionMsgReceivedLiveData.postEvent(ActionMsgReceived(message))
}
override fun onLowOrRaiseHandStatusChanged(userId: Long, isRaiseHand: Boolean) {
if (zoomSDK.inMeetingService.isMyself(userId))
_actionRaiseHandLiveData.postValue(isRaiseHand)
}
override fun onSharingStatus(status: SharingStatus?, userId: Long) {
if (userId == hostId && (status == Sharing_Other_Share_Begin || status == Sharing_Other_Share_End))
_actionActiveVideoLiveData.postValue(ActionActiveVideoUpdated(hostId))
}
override fun toggleMic(isMicOn: Boolean) {
zoomSDK.inMeetingService.inMeetingAudioController.muteMyAudio(isMicOn)
}
override fun toggleVideo(isVideoOn: Boolean) {
val err = zoomSDK.inMeetingService.inMeetingVideoController.muteMyVideo(isVideoOn)
if (err == MobileRTCSDKError.SDKERR_NO_PERMISSION)
_actionVideoToggleDisabledLiveData.postEvent(Unit)
}
override fun toggleRaiseHand(isHandRaised: Boolean) {
if (isHandRaised) zoomSDK.inMeetingService.lowerHand(zoomSDK.inMeetingService.myUserID)
else zoomSDK.inMeetingService.raiseMyHand()
_actionRaiseHandLiveData.postValue(isHandRaised.not())
}
override fun reactEmoji(emojiString: String, emoticon: SDKEmojiReactionType) {
_actionDismissReactionLiveData.postEvent(ActionDismissReactionDialog(emojiString))
if (zoomSDK.inMeetingService.emojiReactionController.isEmojiReactionEnabled)
zoomSDK.inMeetingService.emojiReactionController.sendEmojiReaction(emoticon)
}
override fun sendMessage(message: String, toHostOnly: Boolean) {
if (toHostOnly.not()) zoomSDK.inMeetingService.inMeetingChatController.sendChatToGroup(
MobileRTCChatGroup_All, message
) else zoomSDK.inMeetingService.inMeetingChatController.sendChatToUser(hostId, message)
}
override fun leaveMeeting() = _actionLeaveMeetingLiveData.postEvent(ActionLeaveMeeting)
override fun isOtherUserSharing() =
zoomSDK.inMeetingService.inMeetingShareController.isOtherSharing
override fun onMeetingActiveVideo(userId: Long) {
if (userId == hostId) _actionActiveVideoLiveData.postValue(ActionActiveVideoUpdated(hostId))
}
override fun onMeetingUserUpdated(p0: Long) = Unit
override fun onRecordingStatus(p0: RecordingStatus?) = Unit
override fun onLocalRecordingStatus(p0: RecordingStatus?) = Unit
override fun onActiveVideoUserChanged(p0: Long) = Unit
override fun onMeetingUserJoin(p0: MutableList<Long>?) = Unit
override fun onMeetingUserLeave(p0: MutableList<Long>?) = Unit
override fun onSilentModeChanged(p0: Boolean) = Unit
override fun onMeetingHostChanged(p0: Long) = Unit
override fun onActiveSpeakerVideoUserChanged(userId: Long) = Unit
override fun onSinkAttendeeChatPriviledgeChanged(p0: Int) = Unit
override fun onSinkAllowAttendeeChatNotification(p0: Int) = Unit
override fun onUserNameChanged(p0: Long, p1: String?) = Unit
override fun onFreeMeetingReminder(p0: Boolean, p1: Boolean, p2: Boolean) = Unit
override fun onFreeMeetingNeedToUpgrade(p0: FreeMeetingNeedUpgradeType?, p1: String?) = Unit
override fun onFreeMeetingUpgradeToGiftFreeTrialStart() = Unit
override fun onFreeMeetingUpgradeToGiftFreeTrialStop() = Unit
override fun onFreeMeetingUpgradeToProMeeting() = Unit
override fun onClosedCaptionReceived(p0: String?) = Unit
override fun onMeetingCoHostChanged(p0: Long) = Unit
override fun onUserNetworkQualityChanged(p0: Long) = Unit
override fun onSpotlightVideoChanged(p0: Boolean) = Unit
override fun onWebinarNeedRegister(p0: String?) = Unit
override fun onJoinWebinarNeedUserNameAndEmail(p0: InMeetingEventHandler?) = Unit
override fun onMeetingNeedCloseOtherMeeting(p0: InMeetingEventHandler?) = Unit
override fun onMicrophoneStatusError(p0: MobileRTCMicrophoneError?) = Unit
override fun onUserAudioTypeChanged(p0: Long) = Unit
override fun onMyAudioSourceTypeChanged(p0: Int) = Unit
override fun onInvalidReclaimHostkey() = Unit
override fun onShareActiveUser(p0: Long) = Unit
override fun onShareSettingTypeChanged(p0: ShareSettingType?) = Unit
override fun onShareUserReceivingStatus(userId: Long) = Unit
override fun onMeetingNeedPasswordOrDisplayName(
p0: Boolean, p1: Boolean, p2: InMeetingEventHandler?
) = Unit
}
I have the exact same issue. I am using custom ui, and am not getting the dialog asking about recording permission - although I have seen it in the past. Presumably this is the problem. How do I go about creating a custom recording dialog and letting zoom know that permission to record was granted?
Also @jon.zoom you mention the sample app - could you please send me the link to that? I have found a sample app that contains an example using the default UI but not the custom UI. Thanks.
Thanks for letting me know you have also seen a similar issue. Please do keep me updated if it starts happening again so that we can have a broader set of data points to look into this with.
But now as I try to recreate it, I can’t. When the recording starts, I get a popup informing me about the recording. This is also odd considering I have a custom UI, why is the SDK popping up a dialog? But that’s a separate issue.
This is actually newly introduced behavior. For privacy reasons, the SDK will display a consent dialog whenever a recording begins or a user joins an in-progress recording. This behavior was introduced alongside the new UI legal notices requirement.
Unfortunately the snippet you have provided does not provide a lot of information around how the SDK is being used in your project. Please let me know if you can provide additional details around the direct usages of the SDK so we can investigate further.
Is it possible this new dialog was not working correctly about 4 weeks ago? That’s when I last saw the issue. Perhaps there was a server side issue which caused the dialog not to pop up and therefore locked users out of unmute and start video? Then perhaps the issue got fixed on the server side and the dialog started appearing in the SDK? I went back to the same SDK I was using 4 weeks ago and still can’t recreate so it would seem this was not fixed with an SDK upgrade. There’s something else going on.
I have done some testing and have a workaround to ensure the consent to record dialog always has time to appear. See Event to detect when consent to record confirmed / denied? - #10 by a.diament However it is quite hacky, and I think adding a hook to the sdk to know when the consent was granted, would help a lot.
It isn’t entirely impossible that something would have prevented this dialog from showing, but I am not seeing any incidents that occurred in that timespan. The dialog also should not prevent you from being able to setup a video subscription unless the Leave Meeting button is clicked. I am beginning to suspect that there may be some issues that can arise around incorrect order of operations when joining a meeting with an in-progress recording, but I’ll keep that discussion contained in the other topic.
Definitely keep me updated if you are able to find any way of recreating the video subscription issue that you were seeing a few weeks ago.
I responded in the other topic, so let’s try to keep the two discussions separate for now so that we aren’t double-posting every new piece of data.
If we are able to resolve the consent dialog inconsistencies and are still seeing issues with video subscriptions, we can isolate and revisit this issue. If you do find any new information relevant to the video subscriptions failing that is not related to the consent dialog, please let me know so we can continue investigating.
Thanks for the additional information. Upon further testing I think I found some behavior that may be indicative of what the root cause is. When following the steps to reproduce you’ve provided using the default UI with enableForceAutoStartMyVideoWhenJoinMeeting turned on, a dialog appears saying that the host has disabled video for the meeting. We’ll need to investigate whether this is just an issue of an incorrect error being shown, or altogether unexpected behavior. I’ll keep you updated as we look into this.