Toggling A/v throws SDKERR_NO_PERMISSION

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.

Regards

Hi @rahul.mishra, thanks for the post.

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!

If that was the case how is everything working perfectly when turn the recording off. Also we’ve checked and can ensure that this is not the case.

Hi @rahul.mishra,

Can you please confirm how you are checking that the permission has been granted?

Thanks!

This is called on OnCreate:

  val AUDIO_VIDEO_PERMISSION = arrayOf(RECORD_AUDIO, CAMERA)

  private fun initiateImageCaptureContract() {
        val permissions = AUDIO_VIDEO_PERMISSION

        when {
            PermissionUtils.checkSelfPermissions(this, *permissions) -> getData()
            PermissionUtils.shouldShowRequestPermissionsRationale(this, *permissions) ->
                PermissionUtils.showRationaleDialog(this)
            else -> avPermissionContract.launch(permissions)
        }
    }
object PermissionUtils {

    fun checkSelfPermissions(
        context: Context, vararg permissions: String, action: () -> Unit = {},
    ): Boolean {
        val hasPermission = permissions.all {
            ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
        if (hasPermission) action()
        return hasPermission
    }

    fun shouldShowRequestPermissionsRationale(activity: Activity, vararg permissions: String) =
        permissions.any { ActivityCompat.shouldShowRequestPermissionRationale(activity, it) }

    fun showRationaleDialog(context: Context) {
        MaterialAlertDialogBuilder(context)
            .setTitle(R.string.permission_rationale_dialog_title)
            .setMessage(R.string.permission_rationale_dialog_description)
            .setCancelable(false)
            .setPositiveButton(R.string.permission_rationale_dialog_neutral_action_text) { _, _ ->
                goToSettings(context)
            }.show()
    }

    private fun goToSettings(context: Context) {
        val intent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        intent.data =
            Uri.fromParts(
                context.getString(R.string.package_key_word),
                context.packageName,
                null
            )
        context.startActivity(intent)
    }
}

Hi @rahul.mishra,

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?

Thanks!

Can I share our production app with you to check that? I can share the logs also.

Hi @rahul.mishra,

We would strongly prefer that you send a demo app so that the issue is isolated. Let me know if you have any concerns with this approach.

Thanks!

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.

Hi @vuzix_greg,

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.

Thanks!

Hi @rahul.mishra,

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.

Thanks!

@jon.zoom

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.

Hi @vuzix_greg,

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.

Thanks!

1 Like

Hi @a.diament,

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. :slightly_smiling_face:

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!

@jon.zoom

I finally recreated it consistently. I’m using version 5.7.6.1922 of the Android SDK, although this occurs a few versions back as well.

  1. From Zoom for desktop, start a meeting and begin recording it before SDK app joins.
  2. From an Android SDK app, join the meeting.
  3. Notice you will hear the “recording in progress” audible message on the SDK app but no consent dialog appears.
  4. SDK user is unable to unmute or start video. canUnmuteMyAudio() and canUnmuteMyVideo() both return false.
  5. Stop recording from desktop.
  6. Notice SDK user still cannot unmute or start video.
  7. Start recording from desktop again.
  8. SDK app pops up consent dialog.
  9. SDK user can now unmute and start video with no issues.
1 Like

Hi @vuzix_greg,

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.

Thanks!