/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

import com.amazonaws.services.chime.sdk.meetings.audiovideo.metric.MetricsObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.LocalVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSubscriptionConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.DefaultCameraCaptureSource
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials

/**
 * [AudioVideoControllerFacade] manages the signaling and peer connections.
 */
interface AudioVideoControllerFacade {
    /**
     * Starts audio and video.
     */
    fun start()

    /**
     * Starts audio and video with the given configuration.
     *
     * @param audioVideoConfiguration: [AudioVideoConfiguration] - The configuration to be used for
     * audio and video during a meeting session.
     */
    fun start(audioVideoConfiguration: AudioVideoConfiguration)

    /**
     * Stops audio and video.
     * It's important to call this when your meeting connection is no longer needed
     * in order to clean up and explicitly release resources.
     */
    fun stop()

    /**
     * Subscribe to audio, video, and connection events with an [AudioVideoObserver].
     *
     * @param observer: [AudioVideoObserver] - The observer to subscribe to events with.
     */
    fun addAudioVideoObserver(observer: AudioVideoObserver)

    /**
     * Unsubscribes from audio, video, and connection events by removing specified [AudioVideoObserver].
     *
     * @param observer: [AudioVideoObserver] - The observer to unsubscribe from events with.
     */
    fun removeAudioVideoObserver(observer: AudioVideoObserver)

    /**
     * Subscribe to metrics events with an [MetricsObserver].
     *
     * @param observer: [MetricsObserver] - The observer to subscribe to events with.
     */
    fun addMetricsObserver(observer: MetricsObserver)

    /**
     * Unsubscribes from metrics by removing specified [MetricsObserver].
     *
     * @param observer: [MetricsObserver] - The observer to unsubscribe from events with.
     */
    fun removeMetricsObserver(observer: MetricsObserver)

    /**
     * Start local video and begin transmitting frames from an internally held [DefaultCameraCaptureSource].
     * [stopLocalVideo] will stop the internal capture source if being used.
     *
     * Calling this after passing in a custom [VideoSource] will replace it with the internal capture source.
     *
     * This function will only have effect if [start] has already been called
     */
    fun startLocalVideo()

    /**
     * Start local video with configuration and begin transmitting frames from an internally held [DefaultCameraCaptureSource].
     * [stopLocalVideo] will stop the internal capture source if being used.
     *
     * Calling this after passing in a custom [VideoSource] will replace it with the internal capture source.
     *
     * This function will only have effect if [start] has already been called.
     * Calling this function repeatedly will update configuration of local video.
     * If maxBitRateKbps is not set, it will be self adjusted depending on number of users and videos in the meeting
     *
     * @param config: [LocalVideoConfiguration] - The configuration of emitted video stream, e.g. maxBitRateKbps
     */
    fun startLocalVideo(config: LocalVideoConfiguration)

    /**
     * Start local video with a provided custom [VideoSource] which can be used to provide custom
     * [VideoFrame] objects to be transmitted to remote clients
     *
     * Calling this function repeatedly will replace the previous [VideoSource] as the one being
     * transmitted. It will also stop and replace the internal capture source if [startLocalVideo]
     * was called with no arguments.
     *
     * Read [custom video guide](https://github.com/aws/amazon-chime-sdk-android/blob/master/guides/custom_video.md) for details.
     *
     * @param source: [VideoSource] - The source of video frames to be sent to other clients
     */
    fun startLocalVideo(source: VideoSource)

    /**
     * Start local video with with configuration and a provided custom [VideoSource] which can be used to provide custom
     * [VideoFrame] objects to be transmitted to remote clients
     *
     * Calling this function repeatedly will replace the previous [VideoSource] as the one being
     * transmitted. It will update configuration of local video. It will also stop and replace the internal capture source
     * if [startLocalVideo] was called with no arguments.
     * If maxBitRateKbps is not set, it will be self adjusted depending on number of users and videos in the meeting
     *
     * Read [custom video guide](https://github.com/aws/amazon-chime-sdk-android/blob/master/guides/custom_video.md) for details.
     *
     * @param source: [VideoSource] - The source of video frames to be sent to other clients
     * @param config: [LocalVideoConfiguration] - The configuration of emitted video stream, e.g. maxBitRateKbps
     */
    fun startLocalVideo(source: VideoSource, config: LocalVideoConfiguration)

    /**
     * Stops sending video for local attendee. This will additionally stop the internal capture source if being used.
     * If using a custom video source, this will call [VideoSource.removeVideoSink] on the previously provided source.
     */
    fun stopLocalVideo()

    /**
     * Start remote video.
     */
    fun startRemoteVideo()

    /**
     * Stop remote video.
     */
    fun stopRemoteVideo()

    /**
     * Add, update, or remove subscriptions to remote video sources provided via `remoteVideoSourcesDidBecomeAvailable`.
     *
     * This function requires using the `RemoteVideoSource` provided by `OnRemoteVideoSourcesAvailable`, otherwise it will not update properly.
     * This is what allows to use the `RemoteVideoSource` objects as keys in a map.
     *
     * Including a `RemoteVideoSource` in `addedOrUpdated` which was not previously provided will result in the negotiation of media flow for that source. After negotiation has
     * completed,`videoTileDidAdd` on the tile controller will be called with the `TileState` of the source, and applications
     * can render the video via 'bindVideoTile'. Reincluding a `RemoteVideoSource` can be done to update the provided `VideoSubscriptionConfiguration`,
     * but it is not necessary to continue receiving frames.
     *
     * Including a `RemoteVideoSource` in `removed` will stop the flow video from that source, and lead to a `videoTileDidRemove` call on the
     * tile controller to indicate to the application that the tile should be unbound. To restart the flow of media, the source should be re-added by
     * including in `addedOrUpdated`. Note that videos no longer available in a meeting (i.e. listed in
     * `remoteVideoSourcesDidBecomeUnavailable`) do not need to be removed, as they will be automatically unsubscribed from.
     *
     * Note that before this function is called for the first time, the client will automatically subscribe to all video sources.
     * However this behavior will cease upon first call (e.g. if there are 10 videos in the meeting, the controller will subscribe to all 10, however if
     * `updateVideoSourceSubscriptions` is called with a single video in `addedOrUpdated`, the client will unsubscribe from the other 9.
     * This automatic subscription behavior may be removed in future major version updates, builders should avoid relying on the logic
     * and instead explicitly call `updateVideoSourceSubscriptions` with the sources they want to receive.
     *
     * @param addedOrUpdated: [Map] - updated or added video source configurations.
     * @param removed: [Array] - video sources to remove.
     */
    fun updateVideoSourceSubscriptions(addedOrUpdated: Map<RemoteVideoSource, VideoSubscriptionConfiguration>, removed: Array<RemoteVideoSource>)

    /**
     * Allows an attendee in a Replica meeting to immediately transition to a Primary meeting attendee
     * without need for reconnection.
     *
     *  [PrimaryMeetingPromotionObserver.onPrimaryMeetingPromotion] will be called exactly once on `observer` for each call. If
     *  the promotion is successful,  [PrimaryMeetingPromotionObserver.onPrimaryMeetingDemotion] will be called exactly once
     *  if/when the attendee is demoted. See the observer documentation for possible status codes.
     *
     * Application code may also receive a callback on `AudioVideoObserver.onVideoSessionStarted` without
     * [MeetingSessionStatusCode.VideoAtCapacityViewOnly] to indicate they can begin to share video.
     *
     * `chime::DeleteAttendee` on the Primary meeting attendee will result in [PrimaryMeetingPromotionObserver.onPrimaryMeetingDemotion]
     * being called on `observer` to indicate the attendee is no longer able to share.
     *
     * Any disconnection will trigger an automatic demotion to avoid unexpected or unwanted promotion state on reconnection.
     * This will also call [PrimaryMeetingPromotionObserver.onPrimaryMeetingDemotion];  if the attendee still needs to be
     * an interactive participant in the Primary meeting, [promoteToPrimaryMeeting] should be called again with the same credentials.
     *
     * Note that given the asynchronous nature of this function, this should not be called a second time before
     * [PrimaryMeetingPromotionObserver.onPrimaryMeetingPromotion] is called for the first time. Doing so may result in unexpected
     * behavior.
     *
     * @param credentials The credentials for the primary meeting.  This needs to be obtained out of band.
     * @param observer Function to be called on promotion and possibly demotion; see note above.
     */
    fun promoteToPrimaryMeeting(credentials: MeetingSessionCredentials, observer: PrimaryMeetingPromotionObserver)

    /**
     * Remove the promoted attendee from the Primary meeting. This client will stop sharing audio, video, and data messages.
     * This will revert the end-user to precisely the state they were before a call to [promoteToPrimaryMeeting]
     *
     * This will have no effect if there was no previous successful call to [promoteToPrimaryMeeting]. This
     * may result in [PrimaryMeetingPromotionObserver.onPrimaryMeetingDemotion] but there is no need to wait for that callback
     * to revert UX, etc.
     */
    fun demoteFromPrimaryMeeting()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector.ActiveSpeakerDetectorFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileControllerFacade
import com.amazonaws.services.chime.sdk.meetings.device.DeviceController
import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeControllerFacade

interface AudioVideoFacade : AudioVideoControllerFacade,
    RealtimeControllerFacade,
    DeviceController,
    VideoTileControllerFacade,
    ActiveSpeakerDetectorFacade,
    ContentShareController,
    EventAnalyticsFacade


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatus

/**
 * [AudioVideoObserver] handles audio / video session events.
 *
 * Note: all callbacks will be called on main thread.
 */
interface AudioVideoObserver {
    /**
     * Called when the audio session is connecting or reconnecting.
     *
     * Note: this callback will be called on main thread.
     *
     * @param reconnecting: Boolean - Whether the session is reconnecting or not.
     */
    fun onAudioSessionStartedConnecting(reconnecting: Boolean)

    /**
     * Called when the audio session has started.
     *
     * Note: this callback will be called on main thread.
     *
     * @param reconnecting: Boolean - Whether the session is reconnecting or not.
     */
    fun onAudioSessionStarted(reconnecting: Boolean)

    /**
     * Called when audio session got dropped due to poor network conditions.
     * There will be an automatic attempt of reconnecting it.
     * If the reconnection is successful, [onAudioSessionStarted] will be called with value of reconnecting as true
     *
     * Note: this callback will be called on main thread.
     */
    fun onAudioSessionDropped()

    /**
     * Called when the audio session has stopped with the reason
     * provided in the status. This callback implies that audio client has stopped permanently for this session and there will be
     * no attempt of reconnecting it.
     *
     * Note: this callback will be called on main thread.
     *
     * @param sessionStatus: [MeetingSessionStatus] - The reason why the session has stopped.
     */
    fun onAudioSessionStopped(sessionStatus: MeetingSessionStatus)

    /**
     * Called when audio session cancelled reconnecting.
     *
     * Note: this callback will be called on main thread.
     */
    fun onAudioSessionCancelledReconnect()

    /**
     * Called when the connection health is recovered.
     *
     * Note: this callback will be called on main thread.
     */
    fun onConnectionRecovered()

    /**
     * Called when connection became poor.
     *
     * Note: this callback will be called on main thread.
     */
    fun onConnectionBecamePoor()

    /**
     * Called when the video session is connecting or reconnecting.
     *
     * Note: this callback will be called on main thread.
     */
    fun onVideoSessionStartedConnecting()

    /**
     * Called when the video session has started. Sometimes there is a non fatal error such as
     * trying to send local video when the capacity was already reached. However, user can still
     * receive remote video in the existing video session.
     *
     * Note: this callback will be called on main thread.
     *
     * @param sessionStatus: [MeetingSessionStatus] - Additional details on how the video session started.
     */
    fun onVideoSessionStarted(sessionStatus: MeetingSessionStatus)

    /**
     * Called when the video session has stopped from a started state with the reason
     * provided in the status.
     *
     * Note: this callback will be called on main thread.
     *
     * @param sessionStatus: [MeetingSessionStatus] - The reason why the session has stopped.
     */
    fun onVideoSessionStopped(sessionStatus: MeetingSessionStatus)

    /**
     * Called when remote video source(s) is/are no longer available.
     *
     * Note that these sources do not need to be removed via `updateVideoSourceSubscriptions`,
     * as they will be automatically unsubscribed from.
     *
     * @param sources: [List] - List of RemoteVideoSource objects.
     */
    fun onRemoteVideoSourceUnavailable(sources: List<RemoteVideoSource>)

    /**
     * Called when remote video source(s) is/are now available.
     *
     * Video sources can be explicitly subscribed to through `updateVideoSourceSubscriptions`.
     * These should be stored and used when `updateVideoSourceSubscriptions` is called (i.e. you cannot use `RemoteVideoSource` objects created manually).
     * See `updateVideoSourceSubscriptions` for more information.
     * See note in `updateVideoSourceSubscriptions` documentation for information on
     * subscription behavior if `updateVideoSourceSubscriptions` is never called.
     *
     * @param sources: [List] - List of RemoteVideoSource objects.
     */
    fun onRemoteVideoSourceAvailable(sources: List<RemoteVideoSource>)

    /**
     * Called when video capacity status is updated.
     *
     * Note: this callback will be called on main thread.
     *
     * @param available: Boolean - True if camera send is available (video capacity status is false).
     */
    fun onCameraSendAvailabilityUpdated(available: Boolean)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

import com.amazonaws.services.chime.sdk.meetings.audiovideo.metric.MetricsObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.LocalVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSubscriptionConfiguration
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientController
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientObserver
import com.amazonaws.services.chime.sdk.meetings.internal.metric.ClientMetricsCollector
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientController
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientObserver
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatus
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import java.util.Timer
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.timerTask
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class DefaultAudioVideoController(
    private val audioClientController: AudioClientController,
    private val audioClientObserver: AudioClientObserver,
    private val clientMetricsCollector: ClientMetricsCollector,
    private val configuration: MeetingSessionConfiguration,
    private val videoClientController: VideoClientController,
    private val videoClientObserver: VideoClientObserver
) : AudioVideoControllerFacade {

    companion object {
        private const val PRIMARY_MEETING_PROMOTION_TIMEOUT = 5000L
    }
    private var primaryMeetingPromotionObserver: PrimaryMeetingPromotionObserver? = null

    override fun start() {
        start(AudioVideoConfiguration())
    }

    override fun start(audioVideoConfiguration: AudioVideoConfiguration) {
        audioClientController.start(
            audioFallbackUrl = configuration.urls.audioFallbackURL,
            audioHostUrl = configuration.urls.audioHostURL,
            meetingId = configuration.meetingId,
            attendeeId = configuration.credentials.attendeeId,
            joinToken = configuration.credentials.joinToken,
            audioMode = audioVideoConfiguration.audioMode,
            audioStreamType = audioVideoConfiguration.audioStreamType,
            audioRecordingPresetOverride = audioVideoConfiguration.audioRecordingPresetOverride
        )
        videoClientController.start()
    }

    override fun stop() {
        audioClientController.stop()
        videoClientController.stopAndDestroy()
    }

    override fun startLocalVideo() {
        videoClientController.startLocalVideo()
    }

    override fun startLocalVideo(config: LocalVideoConfiguration) {
        videoClientController.startLocalVideo(config)
    }

    override fun startLocalVideo(source: VideoSource) {
        videoClientController.startLocalVideo(source)
    }

    override fun startLocalVideo(source: VideoSource, config: LocalVideoConfiguration) {
        videoClientController.startLocalVideo(source, config)
    }

    override fun stopLocalVideo() {
        videoClientController.stopLocalVideo()
    }

    override fun startRemoteVideo() {
        videoClientController.startRemoteVideo()
    }

    override fun stopRemoteVideo() {
        videoClientController.stopRemoteVideo()
    }

    override fun addAudioVideoObserver(observer: AudioVideoObserver) {
        audioClientObserver.subscribeToAudioClientStateChange(observer)
        videoClientObserver.subscribeToVideoClientStateChange(observer)
    }

    override fun removeAudioVideoObserver(observer: AudioVideoObserver) {
        audioClientObserver.unsubscribeFromAudioClientStateChange(observer)
        videoClientObserver.unsubscribeFromVideoClientStateChange(observer)
    }

    override fun addMetricsObserver(observer: MetricsObserver) {
        clientMetricsCollector.subscribeToMetrics(observer)
    }

    override fun removeMetricsObserver(observer: MetricsObserver) {
        clientMetricsCollector.unsubscribeFromMetrics(observer)
    }

    override fun updateVideoSourceSubscriptions(
        addedOrUpdated: Map<RemoteVideoSource, VideoSubscriptionConfiguration>,
        removed: Array<RemoteVideoSource>
    ) {
        videoClientController.updateVideoSourceSubscriptions(addedOrUpdated, removed)
    }

    override fun promoteToPrimaryMeeting(
        credentials: MeetingSessionCredentials,
        observer: PrimaryMeetingPromotionObserver
    ) {
        // We create a pseudo-'Wait Group' to wait for both audio and video to complete,
        // and then merge their statuses
        val audioVideoPromotionsToComplete = AtomicInteger()
        audioVideoPromotionsToComplete.addAndGet(2)

        var audioClientStatus = MeetingSessionStatus(MeetingSessionStatusCode.OK)
        var videoClientStatus = MeetingSessionStatus(MeetingSessionStatusCode.OK)
        val mergeAndReturnPromotionStatuses = fun (
            firstStatus: MeetingSessionStatus,
            secondStatus: MeetingSessionStatus
        ) {
            // We intentionally don't differentiate status sources here
            // as the preferences towards audio vs. video really doesn't matter
            val mergedStatus = when {
                firstStatus.statusCode != MeetingSessionStatusCode.OK -> firstStatus
                secondStatus.statusCode != MeetingSessionStatusCode.OK -> secondStatus
                else -> firstStatus
            }

            CoroutineScope(Dispatchers.Main).launch {
                primaryMeetingPromotionObserver?.onPrimaryMeetingPromotion(mergedStatus)
            }
        }

        primaryMeetingPromotionObserver = observer
        // In these observers we try demoting the other client. Note that the individual controllers
        // do not follow the exact same pattern of calling back on observer (with `MeetingSessionStatusCode.OK` in
        // the case of explicit demotion request so we don't need to worry about any infinite loops
        val audioPrimaryMeetingPromotionObserverAdapter = object : PrimaryMeetingPromotionObserver {
            override fun onPrimaryMeetingPromotion(status: MeetingSessionStatus) {
                audioClientStatus = status
                if (audioVideoPromotionsToComplete.decrementAndGet() == 0) {
                    mergeAndReturnPromotionStatuses(audioClientStatus, videoClientStatus)
                }
            }

            override fun onPrimaryMeetingDemotion(status: MeetingSessionStatus) {
                videoClientController.demoteFromPrimaryMeeting()
                CoroutineScope(Dispatchers.Main).launch {
                    primaryMeetingPromotionObserver?.onPrimaryMeetingDemotion(status)
                }
            }
        }
        val videoPrimaryMeetingPromotionObserverAdapter = object : PrimaryMeetingPromotionObserver {
            override fun onPrimaryMeetingPromotion(status: MeetingSessionStatus) {
                videoClientStatus = status
                if (audioVideoPromotionsToComplete.decrementAndGet() == 0) {
                    mergeAndReturnPromotionStatuses(audioClientStatus, videoClientStatus)
                }
            }

            override fun onPrimaryMeetingDemotion(status: MeetingSessionStatus) {
                audioClientController.demoteFromPrimaryMeeting()
                CoroutineScope(Dispatchers.Main).launch {
                    primaryMeetingPromotionObserver?.onPrimaryMeetingDemotion(status)
                }
            }
        }

        audioClientController.promoteToPrimaryMeeting(credentials, audioPrimaryMeetingPromotionObserverAdapter)
        videoClientController.promoteToPrimaryMeeting(credentials, videoPrimaryMeetingPromotionObserverAdapter)

        // Setup timeout in case those callbacks are never hit for extremely spurious network or server issues
        Timer().schedule(timerTask {
            // Setting to -1 will invalidate any late callbacks
            if (audioVideoPromotionsToComplete.getAndSet(-1) != 0) {
                // Try demoting to clean up separate state but remove existing cached observer to avoid repeated callbacks
                primaryMeetingPromotionObserver = null
                videoClientController.demoteFromPrimaryMeeting()
                audioClientController.demoteFromPrimaryMeeting()
                // Using observer from scope
                CoroutineScope(Dispatchers.Main).launch {
                    observer.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioInternalServerError))
                }
            }
        }, PRIMARY_MEETING_PROMOTION_TIMEOUT)
    }

    override fun demoteFromPrimaryMeeting() {
        audioClientController.demoteFromPrimaryMeeting()
        videoClientController.demoteFromPrimaryMeeting()
        CoroutineScope(Dispatchers.Main).launch {
            primaryMeetingPromotionObserver?.onPrimaryMeetingDemotion(MeetingSessionStatus(MeetingSessionStatusCode.OK))
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsObserver
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributes
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingHistoryEvent
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector.ActiveSpeakerDetectorFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector.ActiveSpeakerObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerpolicy.ActiveSpeakerPolicy
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.metric.MetricsObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.LocalVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoRenderView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSubscriptionConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileObserver
import com.amazonaws.services.chime.sdk.meetings.device.DeviceChangeObserver
import com.amazonaws.services.chime.sdk.meetings.device.DeviceController
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice
import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeControllerFacade
import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.TranscriptEventObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.datamessage.DataMessageObserver
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials

class DefaultAudioVideoFacade(
    private val context: Context,
    private val audioVideoController: AudioVideoControllerFacade,
    private val realtimeController: RealtimeControllerFacade,
    private val deviceController: DeviceController,
    private val videoTileController: VideoTileController,
    private val activeSpeakerDetector: ActiveSpeakerDetectorFacade,
    private val contentShareController: ContentShareController,
    private val eventAnalyticsController: EventAnalyticsController
) : AudioVideoFacade {

    private val permissions = arrayOf(
        Manifest.permission.MODIFY_AUDIO_SETTINGS,
        Manifest.permission.RECORD_AUDIO
    )

    override fun start() {
        start(AudioVideoConfiguration())
    }

    override fun start(audioVideoConfiguration: AudioVideoConfiguration) {
        if (audioVideoConfiguration.audioMode != AudioMode.NoDevice) {
            val hasPermission: Boolean = permissions.all {
                ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
            }
            if (!hasPermission) {
                throw SecurityException(
                    "Missing necessary permissions for WebRTC: ${
                        permissions.joinToString(
                            separator = ", ",
                            prefix = "",
                            postfix = ""
                        )
                    }"
                )
            }
        }
        audioVideoController.start(audioVideoConfiguration)
    }

    override fun addAudioVideoObserver(observer: AudioVideoObserver) {
        audioVideoController.addAudioVideoObserver(observer)
    }

    override fun removeAudioVideoObserver(observer: AudioVideoObserver) {
        audioVideoController.removeAudioVideoObserver(observer)
    }

    override fun addMetricsObserver(observer: MetricsObserver) {
        audioVideoController.addMetricsObserver(observer)
    }

    override fun removeMetricsObserver(observer: MetricsObserver) {
        audioVideoController.removeMetricsObserver(observer)
    }

    override fun stop() {
        audioVideoController.stop()
    }

    override fun startLocalVideo() {
        audioVideoController.startLocalVideo()
    }

    override fun startLocalVideo(config: LocalVideoConfiguration) {
        audioVideoController.startLocalVideo(config)
    }

    override fun startLocalVideo(source: VideoSource) {
        audioVideoController.startLocalVideo(source)
    }

    override fun startLocalVideo(source: VideoSource, config: LocalVideoConfiguration) {
        audioVideoController.startLocalVideo(source, config)
    }

    override fun stopLocalVideo() {
        audioVideoController.stopLocalVideo()
    }

    override fun startRemoteVideo() {
        audioVideoController.startRemoteVideo()
    }

    override fun stopRemoteVideo() {
        audioVideoController.stopRemoteVideo()
    }

    override fun updateVideoSourceSubscriptions(
        addedOrUpdated: Map<RemoteVideoSource, VideoSubscriptionConfiguration>,
        removed: Array<RemoteVideoSource>
    ) {
        audioVideoController.updateVideoSourceSubscriptions(addedOrUpdated, removed)
    }

    override fun promoteToPrimaryMeeting(
        credentials: MeetingSessionCredentials,
        observer: PrimaryMeetingPromotionObserver
    ) {
        audioVideoController.promoteToPrimaryMeeting(credentials, observer)
    }

    override fun demoteFromPrimaryMeeting() {
        contentShareController.stopContentShare()
        audioVideoController.demoteFromPrimaryMeeting()
    }

    override fun realtimeLocalMute(): Boolean {
        return realtimeController.realtimeLocalMute()
    }

    override fun realtimeLocalUnmute(): Boolean {
        return realtimeController.realtimeLocalUnmute()
    }

    override fun addRealtimeObserver(observer: RealtimeObserver) {
        realtimeController.addRealtimeObserver(observer)
    }

    override fun removeRealtimeObserver(observer: RealtimeObserver) {
        realtimeController.removeRealtimeObserver(observer)
    }

    override fun realtimeSendDataMessage(topic: String, data: Any, lifetimeMs: Int) {
        realtimeController.realtimeSendDataMessage(topic, data, lifetimeMs)
    }

    override fun addRealtimeDataMessageObserver(topic: String, observer: DataMessageObserver) {
        realtimeController.addRealtimeDataMessageObserver(topic, observer)
    }

    override fun removeRealtimeDataMessageObserverFromTopic(topic: String) {
        realtimeController.removeRealtimeDataMessageObserverFromTopic(topic)
    }

    override fun realtimeSetVoiceFocusEnabled(enabled: Boolean): Boolean {
        return realtimeController.realtimeSetVoiceFocusEnabled(enabled)
    }

    override fun realtimeIsVoiceFocusEnabled(): Boolean {
        return realtimeController.realtimeIsVoiceFocusEnabled()
    }

    override fun listAudioDevices(): List<MediaDevice> {
        return deviceController.listAudioDevices()
    }

    override fun chooseAudioDevice(mediaDevice: MediaDevice) {
        deviceController.chooseAudioDevice(mediaDevice)
    }

    @RequiresApi(Build.VERSION_CODES.N)
    override fun getActiveAudioDevice(): MediaDevice? {
        return deviceController.getActiveAudioDevice()
    }

    override fun getActiveCamera(): MediaDevice? {
        return deviceController.getActiveCamera()
    }

    override fun switchCamera() {
        deviceController.switchCamera()
    }

    override fun addDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceController.addDeviceChangeObserver(observer)
    }

    override fun removeDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceController.removeDeviceChangeObserver(observer)
    }

    override fun bindVideoView(videoView: VideoRenderView, tileId: Int) {
        videoTileController.bindVideoView(videoView, tileId)
    }

    override fun unbindVideoView(tileId: Int) {
        videoTileController.unbindVideoView(tileId)
    }

    override fun addVideoTileObserver(observer: VideoTileObserver) {
        videoTileController.addVideoTileObserver(observer)
    }

    override fun removeVideoTileObserver(observer: VideoTileObserver) {
        videoTileController.removeVideoTileObserver(observer)
    }

    override fun pauseRemoteVideoTile(tileId: Int) {
        videoTileController.pauseRemoteVideoTile(tileId)
    }

    override fun resumeRemoteVideoTile(tileId: Int) {
        videoTileController.resumeRemoteVideoTile(tileId)
    }

    override fun addActiveSpeakerObserver(
        policy: ActiveSpeakerPolicy,
        observer: ActiveSpeakerObserver
    ) {
        activeSpeakerDetector.addActiveSpeakerObserver(policy, observer)
    }

    override fun removeActiveSpeakerObserver(observer: ActiveSpeakerObserver) {
        activeSpeakerDetector.removeActiveSpeakerObserver(observer)
    }

    override fun startContentShare(source: ContentShareSource) {
        contentShareController.startContentShare(source)
    }

    override fun startContentShare(source: ContentShareSource, config: LocalVideoConfiguration) {
        contentShareController.startContentShare(source, config)
    }

    override fun stopContentShare() {
        contentShareController.stopContentShare()
    }

    override fun addContentShareObserver(observer: ContentShareObserver) {
        contentShareController.addContentShareObserver(observer)
    }

    override fun removeContentShareObserver(observer: ContentShareObserver) {
        contentShareController.removeContentShareObserver(observer)
    }

    override fun addEventAnalyticsObserver(observer: EventAnalyticsObserver) {
        eventAnalyticsController.addEventAnalyticsObserver(observer)
    }

    override fun removeEventAnalyticsObserver(observer: EventAnalyticsObserver) {
        eventAnalyticsController.removeEventAnalyticsObserver(observer)
    }

    override fun getMeetingHistory(): List<MeetingHistoryEvent> {
        return eventAnalyticsController.getMeetingHistory()
    }

    override fun getCommonEventAttributes(): EventAttributes {
        return eventAnalyticsController.getCommonEventAttributes()
    }

    override fun addRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {
        realtimeController.addRealtimeTranscriptEventObserver(observer)
    }

    override fun removeRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {
        realtimeController.removeRealtimeTranscriptEventObserver(observer)
    }
}


package com.amazonaws.services.chime.sdk.meetings.audiovideo

import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatus

/**
 * [PrimaryMeetingPromotionObserver] handles events related to the promotion and demotion
 * of attendees initially in replica meetings.
 *
 * Note: all callbacks will be called on main thread.
 */
interface PrimaryMeetingPromotionObserver {
    /**
     * Upon success/failure of [AudioVideoFacade.promoteToPrimaryMeeting] the following observer
     * will return a [MeetingSessionStatus] that will contain a [MeetingSessionStatusCode] of the following:
     *
     * * [MeetingSessionStatusCode.OK]: The promotion was successful (i.e. session token was valid,
     *   there was room in the Primary meeting, etc.), audio will begin flowing
     *   and the attendee can begin to send data messages, and content/video if the call is not already at limit.
     * * [MeetingSessionStatusCode.AudioAuthenticationRejected]: Credentials provided
     *   were invalid when connection attempted to Primary meeting. There may be an issue
     *   with your mechanism which allocates the Primary meeting attendee for the Replica
     *   meeting proxied promotion.  This also may indicate that this API was called in a
     *   non-Replica meeting.
     * * [MeetingSessionStatusCode.AudioCallAtCapacity]: Credentials provided were correct
     *   but there was no room in the Primary meeting.  Promotions to Primary meeting attendee take up a slot, just like
     *   regular Primary meeting attendee connections and are limited by the same mechanisms.
     * * [MeetingSessionStatusCode.AudioServiceUnavailable]: Media has not been connected yet so promotion is not yet possible.
     * * [MeetingSessionStatusCode.AudioInternalServerError]: Other failure, possibly due to disconnect
     *   or timeout. These failures are likely retryable.
     *
     *
     * @param status: [MeetingSessionStatus] - Reason for demotion, see notes above
     */
    fun onPrimaryMeetingPromotion(status: MeetingSessionStatus) {}

    /**
     * This observer callback will only be called for attendees in Replica meetings that have
     * been promoted to the Primary meeting via [AudioVideoFacade.promoteToPrimaryMeeting].
     *
     * Indicates that the client is no longer authenticated to the Primary meeting
     * and can no longer share media. `status` will contain a [MeetingSessionStatusCode] of the following:
     *
     * * [MeetingSessionStatusCode.OK]: [AudioVideoFacade.demoteFromPrimaryMeeting] was used to remove the attendee.
     * * [MeetingSessionStatusCode.AudioAuthenticationRejected]: `chime::DeleteAttendee` was called on the Primary
     *   meeting attendee used in [AudioVideoFacade.promoteToPrimaryMeeting].
     * * [MeetingSessionStatusCode.AudioInternalServerError]: Other failure, possibly due to disconnect
     *   or timeout. These failures are likely retryable. Any disconnection will trigger an automatic
     *   demotion to avoid unexpected or unwanted promotion state on reconnection.
     *
     * @param status: [MeetingSessionStatus] - Reason for demotion, see notes above
     */
    fun onPrimaryMeetingDemotion(status: MeetingSessionStatus) {}
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

/**
 * [SignalStrength] describes the signal strength of an attendee for audio
 */
enum class SignalStrength(val value: Int) {
    /**
     * The attendee has no signal
     */
    None(0),

    /**
     * The attendee has low signal
     */
    Low(1),

    /**
     * The attendee has high signal
     */
    High(2);

    companion object {
        fun from(intValue: Int): SignalStrength? = values().find { it.value == intValue }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

interface TranscriptEvent

/*
 * See [Using Amazon Chime SDK live transcription developer guide](https://docs.aws.amazon.com/chime/latest/dg/process-msgs.html) for details about transcription message types and data guidelines
 */
data class Transcript(val results: Array<TranscriptResult>) : TranscriptEvent {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Transcript

        if (!results.contentEquals(other.results)) return false

        return true
    }

    override fun hashCode(): Int {
        return results.contentHashCode()
    }
}

data class TranscriptResult(
    val resultId: String,
    val channelId: String,
    val isPartial: Boolean,
    val startTimeMs: Long,
    val endTimeMs: Long,
    val alternatives: Array<TranscriptAlternative>,
    val languageCode: String?,
    val languageIdentification: Array<TranscriptLanguageWithScore>?
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as TranscriptResult

        if (!alternatives.contentEquals(other.alternatives)) return false

        return if (languageIdentification == null) {
            other.languageIdentification == null
        } else {
            other.languageIdentification?.let { languageIdentification.contentEquals(it) } ?: false
        }
    }

    override fun hashCode(): Int {
        var result = resultId.hashCode()
        result = 31 * result + channelId.hashCode()
        result = 31 * result + isPartial.hashCode()
        result = 31 * result + startTimeMs.hashCode()
        result = 31 * result + endTimeMs.hashCode()
        result = 31 * result + alternatives.contentHashCode()
        result = 31 * result + languageCode.hashCode()
        languageIdentification?.let {
            it -> result = 31 * result + (it.contentHashCode())
        }
        return result
    }
}

data class TranscriptAlternative(
    val items: Array<TranscriptItem>,
    val entities: Array<TranscriptEntity>?,
    val transcript: String
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as TranscriptAlternative

        if (!items.contentEquals(other.items)) return false
        if (transcript != other.transcript) return false

        return if (entities == null) {
            other.entities == null
        } else {
            other.entities?.let { entities.contentEquals(it) } ?: false
        }
    }

    override fun hashCode(): Int {
        var result = items.contentHashCode()
        result = 31 * result + transcript.hashCode()
        return result
    }
}

data class TranscriptItem(
    val type: TranscriptItemType,
    val startTimeMs: Long,
    val endTimeMs: Long,
    val attendee: AttendeeInfo,
    val content: String,
    val vocabularyFilterMatch: Boolean,
    val confidence: Double?,
    val stable: Boolean?
)

data class TranscriptEntity(
    val category: String,
    val confidence: Double,
    val content: String,
    val startTimeMs: Long,
    val endTimeMs: Long,
    val type: String?
)

data class TranscriptLanguageWithScore(
    val languageCode: String,
    val score: Double
)

data class TranscriptionStatus(
    val type: TranscriptionStatusType,
    val eventTimeMs: Long,
    val transcriptionRegion: String,
    val transcriptionConfiguration: String,
    val message: String
) : TranscriptEvent

enum class TranscriptItemType(val value: Int) {
    Unknown(0),
    Pronunciation(1),
    Punctuation(2);

    companion object {
        fun from(intValue: Int): TranscriptItemType {
            return values().find { it.value == intValue } ?: return Unknown
        }
    }
}

enum class TranscriptionStatusType(val value: Int) {
    Unknown(0),
    Started(1),
    Interrupted(2),
    Resumed(3),
    Stopped(4),
    Failed(5);

    companion object {
        fun from(intValue: Int): TranscriptionStatusType {
            return values().find { it.value == intValue } ?: return Unknown
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo

/**
 * [VolumeLevel] describes the volume level of an attendee for audio
 */
enum class VolumeLevel(val value: Int) {
    /**
     * The attendee is muted
     */
    Muted(-1),

    /**
     * The attendee is not speaking
     */
    NotSpeaking(0),

    /**
     * The attendee is speaking at low volume
     */
    Low(1),

    /**
     * The attendee is speaking at medium volume
     */
    Medium(2),

    /**
     * The attendee is speaking at high volume
     */
    High(3);

    companion object {
        fun from(intValue: Int): VolumeLevel? = values().find { it.value == intValue }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio

/**
 * [AudioMode] describes the audio mode in which the audio client should operate during a meeting session.
 */
enum class AudioMode(val value: Int) {
    /**
     * The mono audio mode with single audio channel and 16KHz sampling rate, for both speaker and microphone.
     */
    Mono16K(1),

    /**
     * The mono audio mode with single audio channel and 48KHz sampling rate, for both speaker and microphone.
     */
    Mono48K(2),

    /**
     * The stereo audio mode with two audio channels for speaker, and single audio channel for microphone,
     * both with 48KHz sampling rate.
     */
    Stereo48K(3),

    NoDevice(4);

    companion object {
        fun from(intValue: Int): AudioMode? = values().find { it.value == intValue }
        fun from(intValue: Int, defaultAudioMode: AudioMode): AudioMode = from(intValue) ?: defaultAudioMode
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio

/**
 * [AudioRecordingPresetOverride] describes the audio recording preset in which
 * the audio client should operate during a meeting session.
 * The values below (except None) directly map to the values defined in:
 * https://github.com/ricardoquesada/android-ndk/blob/master/usr/include/SLES/OpenSLES_AndroidConfiguration.h
 */
enum class AudioRecordingPresetOverride {

    /**
     * No preset override. We will use default preset depending on the mode of operation
     */
    None,

    /**
     * Equivalent of SL_ANDROID_RECORDING_PRESET_GENERIC in openSLES
     */
    Generic,

    /**
     * Equivalent of SL_ANDROID_RECORDING_PRESET_CAMCORDER in openSLES
     */
    Camcorder,

    /**
     * Equivalent of SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION in openSLES
     */
    VoiceRecognition,

    /**
     * Equivalent of SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION in openSLES
     */
    VoiceCommunication
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio

/**
 * [AudioStreamType] describes the audio stream type in which the audio client should operate
 * during a meeting session.
 */
enum class AudioStreamType {

    /**
     * Equivalent of AudioManager.STREAM_VOICE_CALL
     * https://developer.android.com/reference/android/media/AudioManager#STREAM_VOICE_CALL
     */
    VoiceCall,

    /**
     * Equivalent of AudioManager.STREAM_MUSIC
     * https://developer.android.com/reference/android/media/AudioManager#STREAM_MUSIC
     * Note that the meeting session volume cannot be adjusted by the volume button with [Music]
     * except for Oculus Quest 2.
     */
    Music
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector

import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerpolicy.ActiveSpeakerPolicy

/**
 * [ActiveSpeakerDetectorFacade] provides API calls to add and remove
 * active speaker observers.
 */

interface ActiveSpeakerDetectorFacade {

    /**
     * Adds an active speaker observer along with a policy to calculate active speaker scores
     *
     * @param policy: [ActiveSpeakerPolicy]  - Lets you specify a policy to calculate active speaker scores
     * @param observer: [ActiveSpeakerObserver] - Lets you specify an observer to active speaker updates
     */
    fun addActiveSpeakerObserver(
        policy: ActiveSpeakerPolicy,
        observer: ActiveSpeakerObserver
    )

    /**
     * Removes an active speaker observer.
     *
     * @param observer: [ActiveSpeakerObserver] - Lets you remove an observer from active speaker updates
     */
    fun removeActiveSpeakerObserver(observer: ActiveSpeakerObserver)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo

/**
 * [ActiveSpeakerObserver] handles active speaker detection and score changes for attendees.
 */
interface ActiveSpeakerObserver {
    /**
     * @property scoreCallbackIntervalMs: Int? Specifies period (in milliseconds) of updates for
     * onActiveSpeakerScoreChange. If this is null, the observer will not get active
     * speaker score updates. Should be a value greater than 0.
     */
    val scoreCallbackIntervalMs: Int?

    /**
     * Notifies observers of changes to active speaker.
     *
     * @param attendeeInfo: Array<[AttendeeInfo]> - An array of active speakers in decreasing order of score.
     */
    fun onActiveSpeakerDetected(attendeeInfo: Array<AttendeeInfo>)

    /**
     * Periodically sends active speaker scores to observers ONLY IF
     * scoreCallbackIntervalMs is not null
     *
     * @param scores: Map<[AttendeeInfo], Double> - Map of active speakers to respective scores.
     * Scores of 0 mean the attendee is not an active speaker.
     */
    fun onActiveSpeakerScoreChanged(scores: Map<AttendeeInfo, Double>)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalUpdate
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeUpdate
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerpolicy.ActiveSpeakerPolicy
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeObserver
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.ConcurrentHashMap

/**
 * [DefaultActiveSpeakerDetector] A default implementation of the Active Speaker Detector
 *
 * @param audioClientObserver: [AudioClientObserver] - Needed to subscribe the detector
 * to real time events like volume change, attendee mute and attendee unmute
 */

class DefaultActiveSpeakerDetector(
    val audioClientObserver: AudioClientObserver
) : ActiveSpeakerDetectorFacade,
    RealtimeObserver {
    private var speakerScores = ConcurrentHashMap<AttendeeInfo, Double>()
    private var activeSpeakers = mutableListOf<AttendeeInfo>()
    private var mostRecentUpdateTimestamp = mutableMapOf<AttendeeInfo, Long>()
    private var mostRecentAttendeeVolumes = mutableMapOf<AttendeeInfo, VolumeLevel>()
    private var activeSpeakerObservers = mutableSetOf<ActiveSpeakerObserver>()
    private var observerToPolicy = mutableMapOf<ActiveSpeakerObserver, ActiveSpeakerPolicy>()
    private var observerToScoresTimer = mutableMapOf<ActiveSpeakerObserver, Timer>()
    private var activityTimer = Timer("ScheduleActivityTimer", false)
    private val ACTIVITY_WAIT_INTERVAL_MS: Long = 1000
    private val ACTIVITY_UPDATE_INTERVAL_MS: Long = 200
    private val TAG = "ActiveSpeakerDetector"

    init {
        audioClientObserver.subscribeToRealTimeEvents(this)
    }

    override fun addActiveSpeakerObserver(
        policy: ActiveSpeakerPolicy,
        observer: ActiveSpeakerObserver
    ) {
        val isFirstObserver = activeSpeakerObservers.isEmpty()
        activeSpeakerObservers.add(observer)
        observerToPolicy[observer] = policy
        if (isFirstObserver) {
            startActivityTimer()
        }
        startScoresTimerForObserver(observer)
    }

    override fun removeActiveSpeakerObserver(observer: ActiveSpeakerObserver) {
        activeSpeakerObservers.remove(observer)
        observerToPolicy.remove(observer)
        stopScoresTimerForObserver(observer)
        if (activeSpeakerObservers.isEmpty()) {
            stopActivityTimer()
        }
    }

    private fun startActivityTimer() {
        activityTimer = Timer("ScheduleActivityTimer", false)
        activityTimer.scheduleAtFixedRate(
            object : TimerTask() {
                override fun run() {
                    observerToPolicy.values.forEach { policy ->
                        speakerScores.keys.forEach { attendeeInfo ->
                            val lastTimestamp = mostRecentUpdateTimestamp[attendeeInfo] ?: 0
                            if (System.currentTimeMillis() - lastTimestamp > ACTIVITY_WAIT_INTERVAL_MS) {
                                updateScore(
                                    policy,
                                    attendeeInfo,
                                    (mostRecentAttendeeVolumes[attendeeInfo]
                                        ?: VolumeLevel.NotSpeaking)
                                )
                            }
                        }
                    }
                }
            },
            ACTIVITY_UPDATE_INTERVAL_MS,
            ACTIVITY_UPDATE_INTERVAL_MS
        )
    }

    private fun startScoresTimerForObserver(observer: ActiveSpeakerObserver) {
        observer.scoreCallbackIntervalMs?.let {
            val scoresTimer = Timer("ScheduleScoresTimer", false)
            scoresTimer.scheduleAtFixedRate(
                object : TimerTask() {
                    override fun run() {
                        observer.onActiveSpeakerScoreChanged(speakerScores)
                    }
                },
                it.toLong(),
                it.toLong()
            )
            observerToScoresTimer[observer] = scoresTimer
        }
    }

    private fun stopActivityTimer() {
        if (activeSpeakerObservers.isEmpty()) {
            activityTimer.cancel()
        }
    }

    private fun stopScoresTimerForObserver(observer: ActiveSpeakerObserver) {
        observerToScoresTimer[observer]?.let {
            it.cancel()
            observerToScoresTimer.remove(observer)
        }
    }

    private fun updateScore(
        policy: ActiveSpeakerPolicy,
        attendeeInfo: AttendeeInfo,
        volume: VolumeLevel
    ) {
        val activeScore = policy.calculateScore(attendeeInfo, volume)
        if (speakerScores[attendeeInfo] != activeScore) {
            speakerScores[attendeeInfo] = activeScore
            mostRecentUpdateTimestamp[attendeeInfo] = System.currentTimeMillis()
            updateActiveSpeakers(attendeeInfo)
        }
    }

    private fun updateActiveSpeakers(attendeeInfo: AttendeeInfo) {
        if (!shouldUpdateActiveSpeakerList(attendeeInfo)) {
            return
        }
        val sortedSpeakers = speakerScores.filterValues { it != 0.0 }.toList().sortedBy { (_, value) -> value }.map { it.first }
        activeSpeakerObservers.forEach {
            it.onActiveSpeakerDetected(sortedSpeakers.toTypedArray())
        }
        activeSpeakers = sortedSpeakers.toMutableList()
    }

    private fun shouldUpdateActiveSpeakerList(attendeeInfo: AttendeeInfo): Boolean {
        val score = speakerScores[attendeeInfo] ?: 0.0
        return ((score == 0.0 && activeSpeakers.contains(attendeeInfo)) ||
                (score > 0.0 && !activeSpeakers.contains(attendeeInfo)))
    }

    override fun onVolumeChanged(volumeUpdates: Array<VolumeUpdate>) {
        volumeUpdates.forEach {
            mostRecentAttendeeVolumes[it.attendeeInfo] = it.volumeLevel
        }
    }

    override fun onAttendeesJoined(attendeeInfo: Array<AttendeeInfo>) {
        attendeeInfo.forEach {
            speakerScores[it] = 0.0
            mostRecentAttendeeVolumes[it] = VolumeLevel.NotSpeaking
        }
    }

    override fun onAttendeesLeft(attendeeInfo: Array<AttendeeInfo>) {
        attendeeInfo.forEach {
            speakerScores.remove(it)
            mostRecentAttendeeVolumes.remove(it)
            mostRecentUpdateTimestamp.remove(it)
            updateActiveSpeakers(it)
        }
    }

    override fun onAttendeesDropped(attendeeInfo: Array<AttendeeInfo>) {
        onAttendeesLeft(attendeeInfo)
    }

    override fun onSignalStrengthChanged(signalUpdates: Array<SignalUpdate>) {
        // Not needed for active speaker detection as we solely rely on volume levels
    }

    override fun onAttendeesMuted(attendeeInfo: Array<AttendeeInfo>) {
        // Not needed as mute state is propagated in onVolumeChange
    }

    override fun onAttendeesUnmuted(attendeeInfo: Array<AttendeeInfo>) {
        // Not needed as unmute state can be obtained from onVolumeChange
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerpolicy

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel

/**
 * [ActiveSpeakerPolicy] calculates a normalized score of how active a speaker is. Implementations
 * of [ActiveSpeakerPolicy] provide custom algorithms for calculating the score.
 */
interface ActiveSpeakerPolicy {
    /**
     * Return the score of the speaker. If the score is 0, this speaker is not active.
     *
     * @param attendeeInfo: [AttendeeInfo] - Attendee information containing attendeeId and
     * externalUserId
     * @param volume: [VolumeLevel] - Current volume of attendee
     */
    fun calculateScore(attendeeInfo: AttendeeInfo, volume: VolumeLevel): Double
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerpolicy

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import kotlin.math.max

/**
 * [DefaultActiveSpeakerPolicy] A default implementation of the Active Speaker Policy
 *
 * @property speakerWeight Specifies how much weight we give to the existing score of the attendee
 * @property cutoffThreshold Only scores above this threshold will be considered active
 * @property takeoverRate This constant is used to reduce the active speaker score of other attendees
 *                        indicating that the current attendee has "possibly" started speaking because
 *                        the latest volume received was high
 * @property silenceThreshold The threshold number compared with current received volume.
 *                            While calculating the new active speaker score, if the current received
 *                            volume is less than this threshold value, the current received volume is considered as 0,
 *                            else 1.
 */
class DefaultActiveSpeakerPolicy @JvmOverloads constructor(
    private val speakerWeight: Double = 0.9,
    private val cutoffThreshold: Double = 0.01,
    private val takeoverRate: Double = 0.2,
    private val silenceThreshold: Double = 0.2
) : ActiveSpeakerPolicy {
    /**
     * Map of attendees to their current active speaker scores
     */
    private var speakerScores = mutableMapOf<AttendeeInfo, Double>()

    /**
     * normalizeFactor is used to convert the VolumeLevel from 1-3 scale to 0-1 scale
     */
    private val normalizeFactor = 3

    override fun calculateScore(attendeeInfo: AttendeeInfo, volume: VolumeLevel): Double {
        var normalizedVolume: Double = volume.value.toDouble() / normalizeFactor

        if (!speakerScores.containsKey(attendeeInfo)) {
            speakerScores[attendeeInfo] = 0.0
        }

        if (normalizedVolume > silenceThreshold) {
            normalizedVolume = 1.0
        } else {
            normalizedVolume = 0.0
        }

        val score: Double =
            (speakerScores.getValue(attendeeInfo) * speakerWeight) + (normalizedVolume * (1.0 - speakerWeight))
        speakerScores[attendeeInfo] = score

        speakerScores.filterKeys { it != attendeeInfo }.forEach { (otherAttendee, currentScore) ->
            val newOtherAttendeeScore = currentScore - (takeoverRate * normalizedVolume)
            speakerScores[otherAttendee] = max(newOtherAttendeeScore, 0.0)
        }

        if (score < this.cutoffThreshold) {
            return 0.0
        }
        return score
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.LocalVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource

/**
 * [ContentShareController] exposes methods for starting and stopping content share with a [ContentShareSource].
 * The content represents a media steam to be shared in the meeting, such as screen capture or
 * media files.
 * Read [content share guide](https://github.com/aws/amazon-chime-sdk-android/blob/master/guides/content_share.md) for details.
 */
interface ContentShareController {
    /**
     * Start sharing the content of a given [ContentShareSource].
     *
     * Once sharing has started successfully, [ContentShareObserver.onContentShareStarted] will
     * be invoked. If sharing fails or stops, [ContentShareObserver.onContentShareStopped]
     * will be invoked with [ContentShareStatus] as the cause.
     *
     * This will call [VideoSource.addVideoSink] on the provided source
     * and [VideoSource.removeVideoSink] on the previously provided source.
     *
     * Calling this function repeatedly will replace the previous [ContentShareSource] as the one being
     * transmitted.
     *
     * @param source: [ContentShareSource] - The source of content to be shared.
     */
    fun startContentShare(source: ContentShareSource)

    /**
     * Start sharing the content of a given [ContentShareSource] with configuration.
     *
     * Once sharing has started successfully, [ContentShareObserver.onContentShareStarted] will
     * be invoked. If sharing fails or stops, [ContentShareObserver.onContentShareStopped]
     * will be invoked with [ContentShareStatus] as the cause.
     *
     * This will call [VideoSource.addVideoSink] on the provided source
     * and [VideoSource.removeVideoSink] on the previously provided source.
     *
     * Calling this function repeatedly will replace the previous [ContentShareSource] as the one being
     * transmitted, will update the content share configuration, e.g. maxBitRateKbps.
     *
     * @param source: [ContentShareSource] - The source of content to be shared.
     * @param config: [LocalVideoConfiguration] - The configuration of emitted video stream, e.g. maxBitRateKbps
     */
    fun startContentShare(source: ContentShareSource, config: LocalVideoConfiguration)

    /**
     * Stop sharing the content of a [ContentShareSource] that previously started.
     *
     * Once the sharing stops successfully, [ContentShareObserver.onContentShareStopped]
     * will be invoked with status code [ContentShareStatusCode.OK].
     *
     */
    fun stopContentShare()

    /**
     * Subscribe the given observer to content share events (sharing started and stopped).
     *
     * @param observer: [ContentShareObserver] - The observer to be notified for events.
     */
    fun addContentShareObserver(observer: ContentShareObserver)

    /**
     * Unsubscribe the given observer from content share events.
     *
     * @param observer: [ContentShareObserver] - The observer to be removed for events.
     */
    fun removeContentShareObserver(observer: ContentShareObserver)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare

/**
 * [ContentShareObserver] handles all callbacks related to the content share.
 * By implementing the callback functions and registering with [ContentShareController.addContentShareObserver],
 * one can get notified with content share status events.
 */
interface ContentShareObserver {
    /**
     * Called when the content share has started.
     * This callback will be on the main thread.
     */
    fun onContentShareStarted()

    /**
     * Called when the content is no longer shared with other attendees
     * with the reason provided in the status.
     *
     * If you no longer need the source producing frames,
     * most builders can stop the source after this callback is invoked.
     *
     * @param status: [ContentShareStatus] - the reason why the content share has stopped
     */
    fun onContentShareStopped(status: ContentShareStatus)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource

/**
 * [ContentShareSource] contains the media sources to attach to the content share
 */
open class ContentShareSource {
    open var videoSource: VideoSource? = null
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare

/**
 * [ContentShareStatus] indicates a status received regarding the content share.
 *
 * @param statusCode: [ContentShareStatusCode] - Additional details for the status
 */
data class ContentShareStatus(val statusCode: ContentShareStatusCode)


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare

/**
 * [ContentShareStatusCode] indicates the reason the content share event occurred.
 */
enum class ContentShareStatusCode {
    /**
     * No failure.
     */
    OK,

    /**
     * Content share video connection is in an unrecoverable failed state.
     * Restart content share connection when this error is encountered.
     */
    VideoServiceFailed;
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.LocalVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.internal.contentshare.ContentShareVideoClientController
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

class DefaultContentShareController(
    private val logger: Logger,
    private val contentShareVideoClientController: ContentShareVideoClientController
) : ContentShareController {

    private val TAG = "DefaultContentShareController"

    override fun startContentShare(source: ContentShareSource) {
        source.videoSource?.let {
            contentShareVideoClientController.startVideoShare(it)
        }
    }

    override fun startContentShare(source: ContentShareSource, config: LocalVideoConfiguration) {
        source.videoSource?.let {
            contentShareVideoClientController.startVideoShare(it, config)
        }
    }

    override fun stopContentShare() {
        contentShareVideoClientController.stopVideoShare()
    }

    override fun addContentShareObserver(observer: ContentShareObserver) {
        contentShareVideoClientController.subscribeToVideoClientStateChange(observer)
    }

    override fun removeContentShareObserver(observer: ContentShareObserver) {
        contentShareVideoClientController.unsubscribeFromVideoClientStateChange(observer)
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.metric

/**
 * [MetricsObserver] handles events related to audio/video metrics.
 *
 * Note: all callbacks will be called on main thread.
 */
interface MetricsObserver {
    /**
     * Called when metrics are received.
     *
     * Note: this callback will be called on main thread.
     *
     * @param metrics: Map<ObservableMetric, Any> - Map of metric type to value
     */
    fun onMetricsReceived(metrics: Map<ObservableMetric, Any>)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.metric

/**
 * [ObservableMetric] represents filtered metrics that are intended to propagate to the
 * top level observers. All metrics are measured over the past second.
 */
enum class ObservableMetric {
    /**
     * Percentage of audio packets lost from server to client
     */
    audioReceivePacketLossPercent,

    /**
     * Percentage of audio packets lost from client to server
     */
    audioSendPacketLossPercent,

    /**
     * Estimated uplink bandwidth from perspective of video client
     */
    videoAvailableSendBandwidth,

    /**
     * Estimated downlink bandwidth  from perspective of video client
     */
    videoAvailableReceiveBandwidth,

    /**
     * Sum of total bitrate across all send streams
     */
    videoSendBitrate,

    /**
     * Percentage of video packets lost from client to server across all send streams
     */
    videoSendPacketLossPercent,

    /**
     * Average send FPS across all send streams
     */
    videoSendFps,

    /**
     * Round trip time of packets sent from client to server
     */
    videoSendRttMs,

    /**
     * Sum of total bitrate across all receive streams
     */
    videoReceiveBitrate,

    /**
     * Percentage of video packets lost from server to client across all receive streams
     */
    videoReceivePacketLossPercent,

    /* Below are metrics for content share stream */

    /**
     * Sum of total bitrate across all send streams
     */
    contentShareVideoSendBitrate,

    /**
     * Percentage of video packets lost from client to server across all send streams
     */
    contentShareVideoSendPacketLossPercent,

    /**
     * Average send FPS across all send streams
     */
    contentShareVideoSendFps,

    /**
     * Round trip time of packets sent from client to server
     */
    contentShareVideoSendRttMs;

    /**
     * Determine if the metric is for content share stream
     */
    fun isContentShareMetric() = this.name.startsWith("contentShare")
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import android.content.Context
import android.util.AttributeSet
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.SurfaceRenderView

class DefaultVideoRenderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : SurfaceRenderView(context, attrs, defStyle)


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

class DefaultVideoTile(
    private val logger: Logger,
    tileId: Int,
    attendeeId: String,
    videoStreamContentWidth: Int,
    videoStreamContentHeight: Int,
    isLocalTile: Boolean
) : VideoTile {
    private val TAG = "DefaultVideoTile"

    override var state: VideoTileState = VideoTileState(tileId,
                                                        attendeeId,
                                                        videoStreamContentWidth,
                                                        videoStreamContentHeight,
                                                        VideoPauseState.Unpaused,
                                                        isLocalTile)
    override var videoRenderView: VideoRenderView? = null

    override fun bind(videoRenderView: VideoRenderView?) {
        logger.info(TAG, "Binding the View to Tile")
        this.videoRenderView = videoRenderView
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        videoRenderView?.onVideoFrameReceived(frame)
    }

    override fun unbind() {
        logger.info(TAG, "Unbinding the View from Tile")
        videoRenderView = null
    }

    override fun setPauseState(pauseState: VideoPauseState) {
        this.state.pauseState = pauseState
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingStatsCollector
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglVideoRenderView
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ConcurrentSet
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ObserverUtils
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientController
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

class DefaultVideoTileController(
    private val logger: Logger,
    private val videoClientController: VideoClientController,
    private val videoTileFactory: VideoTileFactory,
    private val eglCoreFactory: EglCoreFactory,
    private val meetingStatsCollector: MeetingStatsCollector
) : VideoTileController {
    // A map of tile id to VideoTile to determine if VideoTileController is adding, removing, pausing, or rendering
    private val videoTileMap = mutableMapOf<Int, VideoTile>()
    // A map of VideoRenderView to VideoTile to determine if users are adding same video render view
    private val renderViewToBoundVideoTileMap = mutableMapOf<VideoRenderView, VideoTile>()
    private val TAG = "DefaultVideoTileController"

    private var videoTileObservers = ConcurrentSet.createConcurrentSet<VideoTileObserver>()

    override fun onReceiveFrame(
        frame: VideoFrame?,
        videoId: Int,
        attendeeId: String?,
        pauseState: VideoPauseState
    ) {
        /**
         * There are FOUR possible outcomes:
         * 1) Create - Someone has started sharing video
         * 2) Render / Resume - Someone is sending new frames for their video
         * 3) Pause - Someone is sending a pause frame
         * 4) Stop - Someone has stopped sharing video
         *
         * In both pause and stop cases, the frame is null but the pauseType differs
         */
        val tile: VideoTile? = videoTileMap[videoId]

        val videoStreamContentWidth = frame?.width ?: 0
        val videoStreamContentHeight = frame?.height ?: 0

        if (tile != null) {
            if (frame == null && pauseState == VideoPauseState.Unpaused) {
                logger.info(
                    TAG,
                    "Removing video tile with videoId = $videoId & attendeeId = $attendeeId"
                )
                onRemoveVideoTile(videoId)
                return
            }

            if (frame != null && (videoStreamContentWidth != tile.state.videoStreamContentWidth ||
                videoStreamContentHeight != tile.state.videoStreamContentHeight)) {
                tile.state.videoStreamContentWidth = videoStreamContentWidth
                tile.state.videoStreamContentHeight = videoStreamContentHeight
                forEachObserver { observer -> observer.onVideoTileSizeChanged(tile.state) }
            }

            // Account for any internally changed pause states, but ignore if the tile is paused by
            // user since the pause might not have propagated yet
            if (pauseState != tile.state.pauseState && tile.state.pauseState != VideoPauseState.PausedByUserRequest) {
                // Note that currently, since we preemptively mark tiles as PausedByUserRequest when requested by user
                // this path will only be hit when we are either transitioning from .unpaused to PausedForPoorConnection
                // or PausedForPoorConnection to Unpaused
                tile.setPauseState(pauseState)
                if (pauseState == VideoPauseState.Unpaused) {
                    forEachObserver { observer -> observer.onVideoTileResumed(tile.state) }
                } else {
                    forEachObserver { observer -> observer.onVideoTilePaused(tile.state) }
                }
            }

            // Ignore any frames which come to an already paused tile
            if (tile.state.pauseState == VideoPauseState.Unpaused) {
                frame?.run { tile.onVideoFrameReceived(frame) }
            } else {
                logger.verbose(TAG, "Ignoring video frame received on paused tile")
            }
        } else {
            if (frame != null || pauseState != VideoPauseState.Unpaused) {
                run {
                    logger.info(
                        TAG,
                        "Adding video tile with videoId = $videoId, attendeeId = $attendeeId, pauseState = $pauseState"
                    )
                    onAddVideoTile(videoId, attendeeId, pauseState, videoStreamContentWidth, videoStreamContentHeight)
                }
            }
        }
    }

    override fun addVideoTileObserver(observer: VideoTileObserver) {
        videoTileObservers.add(observer)
    }

    override fun removeVideoTileObserver(observer: VideoTileObserver) {
        videoTileObservers.remove(observer)
    }

    override fun pauseRemoteVideoTile(tileId: Int) {
        videoTileMap[tileId]?.let {
            if (it.state.isLocalTile) {
                logger.warn(TAG, "Cannot pause local video tile $tileId!")
                return
            }

            logger.info(TAG, "Pausing tile $tileId")
            videoClientController.setRemotePaused(
                true,
                tileId
            )
            // Don't update state/observers if we haven't changed anything
            // Note that this will overwrite PausedForPoorConnection if that is the current state
            if (it.state.pauseState != VideoPauseState.PausedByUserRequest) {
                it.setPauseState(VideoPauseState.PausedByUserRequest)
                forEachObserver { observer -> observer.onVideoTilePaused(it.state) }
            }
        }
    }

    override fun resumeRemoteVideoTile(tileId: Int) {
        videoTileMap[tileId]?.let {
            if (it.state.isLocalTile) {
                logger.warn(TAG, "Cannot resume local video tile $tileId!")
                return
            }

            logger.info(TAG, "Resuming tile $tileId")
            videoClientController.setRemotePaused(
                false,
                tileId
            )
            // Only update state if we are unpausing a tile which was previously paused by the user
            // Note that this means resuming a tile with state PausedForPoorConnection will no-op
            if (it.state.pauseState == VideoPauseState.PausedByUserRequest) {
                it.setPauseState(VideoPauseState.Unpaused)
                forEachObserver { observer -> observer.onVideoTileResumed(it.state) }
            }
        }
    }

    override fun bindVideoView(videoView: VideoRenderView, tileId: Int) {
        logger.info(TAG, "Binding VideoView to Tile with tileId = $tileId")

        renderViewToBoundVideoTileMap[videoView]?.let {
            if (it.state.tileId == tileId) {
                logger.info(TAG, "Already binding with the tile Id $tileId, ignore...")
                return
            }
            logger.warn(TAG, "Override the binding from ${it.state.tileId} to $tileId")
            removeRenderViewFromBoundVideoTileMap(it.state.tileId)
        }

        videoTileMap[tileId]?.let {
            it.videoRenderView?.let {
                logger.info(TAG, "tileId = $tileId already had a different video view. Unbinding the old one and associating the new one")
                removeRenderViewFromBoundVideoTileMap(tileId)
            }
            if (videoView is EglVideoRenderView) {
                logger.info(TAG, "Initializing EGL state on EGL render view")
                videoView.init(eglCoreFactory)
            }
            it.bind(videoView)
            renderViewToBoundVideoTileMap[videoView] = it
        }
    }

    private fun removeRenderViewFromBoundVideoTileMap(tileId: Int) {
        renderViewToBoundVideoTileMap.entries.firstOrNull { it.value.state.tileId == tileId }?.let {
            val renderView = it.key
            val videoTile = it.value
            videoTile.unbind()
            if (renderView is EglVideoRenderView) {
                logger.info(TAG, "Releasing EGL state on EGL render view")
                renderView.release()
            }
            renderViewToBoundVideoTileMap.remove(renderView)
        }
    }

    override fun unbindVideoView(tileId: Int) {
        logger.info(TAG, "Unbinding Tile with tileId = $tileId")
        // Remove the video from both mappings when unbind, in order to keep the old SDK behavior
        videoTileMap.remove(tileId)
        removeRenderViewFromBoundVideoTileMap(tileId)
    }

    private fun onRemoveVideoTile(tileId: Int) {
        videoTileMap[tileId]?.let {
            forEachObserver { observer -> observer.onVideoTileRemoved(it.state) }
            videoTileMap.remove(tileId)
        }
    }

    private fun onAddVideoTile(tileId: Int, attendeeId: String?, pauseState: VideoPauseState, videoStreamContentWidth: Int, videoStreamContentHeight: Int) {
        val isLocalTile: Boolean
        val thisAttendeeId: String

        if (attendeeId != null) {
            thisAttendeeId = attendeeId
            isLocalTile = false
        } else {
            thisAttendeeId = videoClientController.getConfiguration().credentials.attendeeId
            isLocalTile = true
        }
        val tile = videoTileFactory.makeTile(tileId, thisAttendeeId, videoStreamContentWidth, videoStreamContentHeight, isLocalTile)
        videoTileMap[tileId] = tile
        meetingStatsCollector.updateMaxVideoTile(videoTileMap.size)
        tile.setPauseState(pauseState)
        forEachObserver { observer -> observer.onVideoTileAdded(tile.state) }
    }

    private fun forEachObserver(observerFunction: (observer: VideoTileObserver) -> Unit) {
        ObserverUtils.notifyObserverOnMainThread(videoTileObservers, observerFunction)
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

class DefaultVideoTileFactory(private val logger: Logger) : VideoTileFactory {
    override fun makeTile(
        tileId: Int,
        attendeeId: String,
        videoStreamContentWidth: Int,
        videoStreamContentHeight: Int,
        isLocalTile: Boolean
    ): VideoTile {
        return DefaultVideoTile(logger, tileId, attendeeId, videoStreamContentWidth, videoStreamContentHeight, isLocalTile)
    }
}


package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * Contains configuration for a local video or content share to be sent
 *
 * @property maxBitRateKbps: Int - The max bit rate for video encoding, should be greater than 0
 * Actual quality achieved may vary throughout the call depending on what system and network can provide
 */
data class LocalVideoConfiguration @JvmOverloads constructor(
    private val maxBitRateKbps: Int = 0
) {
    val safeMaxBitRateKbps: Int
    get() = if (maxBitRateKbps < 0) 0 else maxBitRateKbps
}


package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * A video source available in the current meeting. RemoteVideoSource need to be consistent between `remoteVideoSourcesDidBecomeAvailable`
 * and `updateVideoSourceSubscriptions` as they are used as keys in maps that may be updated.
 * I.e. when setting up a map for `updateVideoSourceSubscriptions` do not construct RemoteVideoSource yourselves
 * or the configuration may or may not be updated.
 *
 * @property attendeeId: [String] - The attendee ID this video tile belongs to. Note that screen share video will have a suffix of #content
 */
class RemoteVideoSource(val attendeeId: String)


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * [VideoContentHint] describes the content type of a video source so that downstream encoders, etc. can properly
 * decide on what parameters will work best. These options mirror https://www.w3.org/TR/mst-content-hint/ .
 */
enum class VideoContentHint {
    /**
     * No hint has been provided.
     */
    None,

    /**
     * The track should be treated as if it contains video where motion is important.
     *
     * This is normally webcam video, movies or video games.
     */
    Motion,

    /**
     * The track should be treated as if video details are extra important.
     *
     * This is generally applicable to presentations or web pages with text content, painting or line art.
     */
    Detail,

    /**
     * The track should be treated as if video details are extra important, and that
     * significant sharp edges and areas of consistent color can occur frequently.
     *
     * This is generally applicable to presentations or web pages with text content.
     */
    Text
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameBuffer
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.DefaultSurfaceTextureCaptureSource

/**
 * [VideoFrame] is a class which contains a [VideoFrameBuffer] and metadata necessary for transmission
 * Typically produced via a [VideoSource] and consumed via a [VideoSink]
 */
class VideoFrame(
    /**
     * Timestamp in nanoseconds at which the video frame was captured from some system monotonic clock.
     * Will be aligned and converted to NTP (Network Time Protocol) within AmazonChimeSDKMedia library, which will then
     * be converted to a system monotonic clock on remote end. May be different on frames emanated from AmazonChimeSDKMedia library.
     *
     * See [DefaultSurfaceTextureCaptureSource] for usage of a AmazonChimeSDKMedia library class which can convert to the clock used by the library
     */
    val timestampNs: Long,

    /**
     * Object containing actual video frame data in some form
     */
    val buffer: VideoFrameBuffer,

    /**
     * Rotation of the video frame buffer in degrees clockwise
     * from intended viewing horizon.
     *
     * e.g. If you were recording camera capture upside down relative to
     * the orientation of the sensor, this value would be [VideoRotation.Rotation180].
     */
    val rotation: VideoRotation = VideoRotation.Rotation0
) {
    /**
     * Width of the video frame
     *
     * @return [Int] - Frame width in pixels
     */
    val width: Int
        get() = buffer.width

    /**
     * Height of the video frame
     *
     * @return [Int] - Frame height in pixels
     */
    val height: Int
        get() = buffer.height

    /**
     * Width of frame when the reverse of [rotation] is applied to the buffer
     * e.g. a frame with width = 1 and height = 2 with 90 degrees
     * rotation will have rotated width = 2 and height = 1
     *
     * @return [Int] - Frame width when rotation is removed
     */
    fun getRotatedWidth(): Int {
        return if (rotation.degrees % 180 == 0) {
            buffer.width
        } else {
            buffer.height
        }
    }

    /**
     * Height of frame when the reverse of [rotation] is applied to the buffer
     * e.g. a frame with width = 1 and height = 2 with 90 degrees
     * rotation will have rotated width = 2 and height = 1
     *
     * @return [Int] - Frame height when rotation is removed
     */
    fun getRotatedHeight(): Int {
        return if (rotation.degrees % 180 == 0) {
            buffer.height
        } else {
            buffer.width
        }
    }

    /**
     * Helper function to call [VideoFrameBuffer.retain] on the owned buffer
     */
    fun retain() = buffer.retain()

    /**
     * Helper function to call [VideoFrameBuffer.release] on the owned buffer
     */
    fun release() = buffer.release()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * [VideoPauseState] describes the pause status of a video tile.
 */
enum class VideoPauseState(val value: Int) {
    /**
     * The video tile is not paused
     */
    Unpaused(0),

    /**
     * The video tile has been paused by the user, and will only be unpaused if the user requests it to resume.
     */
    PausedByUserRequest(1),

    /**
     * The video tile has been paused to save on local downlink bandwidth. When the connection improves,
     * it will be automatically unpaused by the client. User requested pauses will shadow this pause,
     * but if the connection has not recovered on resume the tile will still be paused with this state.
     */
    PausedForPoorConnection(2);

    companion object {
        fun from(intValue: Int): VideoPauseState? = values().find { it.value == intValue }
    }
}


package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * Enum defining video priority for remote video sources. The 'higher' the number the 'higher' the priority for the source when adjusting video quality
 * to adapt to variable network conditions, i.e. `Highest` will be chosen before `High`, `Medium`, etc.
 */
enum class VideoPriority(val value: Int) {
    Lowest(0),
    Low(10),
    Medium(20),
    High(30),
    Highest(40);
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * [VideoRenderView] is the type of VideoSink used by the [VideoTileController]
 */
interface VideoRenderView : VideoSink


package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * Customizable video resolution parameters for a remote video source.
 */
enum class VideoResolution(val width: Int, val height: Int) {
    Low(360, 240),
    Medium(640, 480),
    High(960, 720);
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * [VideoRotation] describes the rotation of the video frame buffer in degrees clockwise
 * from intended viewing horizon.
 *
 * e.g. If you were recording camera capture upside down relative to
 * the orientation of the sensor, this value would be [VideoRotation.Rotation180].
 */
enum class VideoRotation(val degrees: Int) {
    /**
     * Not rotated
     */
    Rotation0(0),

    /**
     * Rotated 90 degrees clockwise
     */
    Rotation90(90),

    /**
     * Rotated 180 degrees clockwise
     */
    Rotation180(180),

    /**
     * Rotated 270 degrees clockwise
     */
    Rotation270(270);

    companion object {
        fun from(intValue: Int): VideoRotation? = values().find { it.degrees == intValue }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * [VideoScalingType] describes the scaling type of how video is rendered.  Certain types
 * may effect how much of a video is cropped. [visibleFraction] refers to the minimum amount
 * of a video frame required to be shown per scaling type (e.g. [AspectFit] indicates showing
 * the whole frame, no cropping).
 */
enum class VideoScalingType(val visibleFraction: Float) {
    /**
     * Fit the frame to the surrounding view to avoid any cropping.
     */
    AspectFit(1.0f),

    /**
     * Attempt to avoid cropping seen using [AspectFill] while showing more
     * of the image then [AspectFit]; this may crop if the aspect ratios do not match.
     */
    AspectBalanced(0.5625f),

    /**
     * Fill the surrounding view; this may crop if the aspect ratios do not match.
     */
    AspectFill(0.0f)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * A [VideoSink] consumes video frames, typically from a [VideoSource]. It may process, fork, or render these frames.
 * Typically connected via [VideoSource.addVideoSink] and disconnected via [VideoSource.removeVideoSink]
 */
interface VideoSink {
    /**
     * Receive a video frame from some upstream source.
     * The [VideoSink] may render, store, process, and forward the frame, among other applications.
     *
     * @param frame: [VideoFrame] - New video frame to consume
     */
    fun onVideoFrameReceived(frame: VideoFrame)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade

/**
 * [VideoSource] is an interface for sources which produce video frames, and can send to a [VideoSink].
 * Implementations can be passed to the [AudioVideoFacade] to be used as the video source sent to remote
 * participants
 */
interface VideoSource {
    /**
     * Add a video sink which will immediately begin to receive new frames.
     *
     * Multiple sinks can be added to a single [VideoSource] to allow forking of video frames,
     * e.g. to send to both local preview and AmazonChimeSDKMedia library (for encoding) at the same time.
     *
     * @param sink: [VideoSink] - New video sink
     */
    fun addVideoSink(sink: VideoSink)

    /**
     * Remove a video sink which will no longer receive new frames on return
     *
     * @param sink: [VideoSink] - Video sink to remove
     */
    fun removeVideoSink(sink: VideoSink)

    /**
     * Content hint for downstream processing
     */
    val contentHint: VideoContentHint
}


package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * Configuration for a specific video source.
 * The values are intentionally mutable so that a map of all current configurations can be kept and updated as needed.
 *
 * VideoSubscriptionConfiguration is used to contain the priority and resolution of
 * remote video sources and content share to be received
 * @property priority: [VideoPriority] - Relative priority for the subscription. See [VideoPriority] for more information.
 * @property targetResolution: [VideoResolution] - A target resolution for the subscription. The actual receive resolution may vary.
 */
class VideoSubscriptionConfiguration(var priority: VideoPriority, var targetResolution: VideoResolution)


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

/**
 * [VideoTile] is a tile that binds video render view to display the frame into the view.
 */
interface VideoTile : VideoSink {
    /**
     * State of video tile
     */
    var state: VideoTileState

    /**
     * View which will be used to render the Video Frame
     */
    var videoRenderView: VideoRenderView?

    /**
     * Binds the view to the tile. The view needs to be create by the application.
     * Once the binding is done, the view will start displaying the video frame automatically
     *
     * @param videoRenderView: [VideoRenderView] - The view created by application to render the video frame
     */
    fun bind(videoRenderView: VideoRenderView?)

    /**
     * Unbinds the [videoRenderView] from tile.
     */
    fun unbind()

    /**
     * Update the pause state of the tile.
     */
    fun setPauseState(pauseState: VideoPauseState)
}