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

import AVFoundation
import Foundation

@objcMembers public class DefaultAudioVideoController: NSObject, AudioVideoControllerFacade {
    public let configuration: MeetingSessionConfiguration
    public let logger: Logger

    private let audioClientController: AudioClientController
    private let audioClientObserver: AudioClientObserver
    private let clientMetricsCollector: ClientMetricsCollector
    private var videoClientController: VideoClientController
    private let videoTileController: VideoTileController
    private var primaryMeetingPromotionObserver: PrimaryMeetingPromotionObserver?

    public init(audioClientController: AudioClientController,
                audioClientObserver: AudioClientObserver,
                clientMetricsCollector: ClientMetricsCollector,
                videoClientController: VideoClientController,
                videoTileController: VideoTileController,
                configuration: MeetingSessionConfiguration,
                logger: Logger) {
        self.audioClientController = audioClientController
        self.audioClientObserver = audioClientObserver
        self.clientMetricsCollector = clientMetricsCollector
        self.videoClientController = videoClientController
        self.videoTileController = videoTileController
        self.configuration = configuration
        self.logger = logger
    }

    public func start() throws {
        // By default, start for calls without CallKit integration. Use start(callKitEnabled:)
        // to override the default behavior if the call is integrated with CallKit
        try self.start(audioVideoConfiguration: AudioVideoConfiguration())
    }

    public func start(callKitEnabled: Bool) throws {
        try self.start(audioVideoConfiguration: AudioVideoConfiguration(callKitEnabled: callKitEnabled))
    }

    public func start(audioVideoConfiguration: AudioVideoConfiguration) throws {
        try audioClientController.start(audioFallbackUrl: configuration.urls.audioFallbackUrl,
                                        audioHostUrl: configuration.urls.audioHostUrl,
                                        meetingId: configuration.meetingId,
                                        attendeeId: configuration.credentials.attendeeId,
                                        joinToken: configuration.credentials.joinToken,
                                        callKitEnabled: audioVideoConfiguration.callKitEnabled,
                                        audioMode: audioVideoConfiguration.audioMode)
        videoClientController.subscribeToVideoTileControllerObservers(observer: videoTileController)
        videoClientController.start()
    }

    public func stop() {
        audioClientController.stop()
        videoClientController.stopAndDestroy()
    }

    public func addAudioVideoObserver(observer: AudioVideoObserver) {
        audioClientObserver.subscribeToAudioClientStateChange(observer: observer)
        videoClientController.subscribeToVideoClientStateChange(observer: observer)
    }

    public func removeAudioVideoObserver(observer: AudioVideoObserver) {
        audioClientObserver.unsubscribeFromAudioClientStateChange(observer: observer)
        videoClientController.unsubscribeFromVideoClientStateChange(observer: observer)
    }

    public func addMetricsObserver(observer: MetricsObserver) {
        clientMetricsCollector.subscribeToMetrics(observer: observer)
    }

    public func removeMetricsObserver(observer: MetricsObserver) {
        clientMetricsCollector.unsubscribeFromMetrics(observer: observer)
    }

    public func startLocalVideo() throws {
        try videoClientController.startLocalVideo()
    }

    public func startLocalVideo(config: LocalVideoConfiguration) throws {
        try videoClientController.startLocalVideo(config: config)
    }

    public func startLocalVideo(source: VideoSource) {
        videoClientController.startLocalVideo(source: source)
    }

    public func startLocalVideo(source: VideoSource, config: LocalVideoConfiguration) {
        videoClientController.startLocalVideo(source: source, config: config)
    }

    public func stopLocalVideo() {
        videoClientController.stopLocalVideo()
    }

    public func startRemoteVideo() {
        videoClientController.startRemoteVideo()
    }

    public func stopRemoteVideo() {
        videoClientController.stopRemoteVideo()
    }

    public func updateVideoSourceSubscriptions(addedOrUpdated: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration>, removed: Array<RemoteVideoSource>) {
        videoClientController.updateVideoSourceSubscriptions(addedOrUpdated: addedOrUpdated, removed: removed)
    }

    public func promoteToPrimaryMeeting(
        credentials: MeetingSessionCredentials,
        observer: PrimaryMeetingPromotionObserver) {
        let group = DispatchGroup()
        var videoClientPromotionStatus: MeetingSessionStatus?
        let videoClientPromotionCallback = { (status: MeetingSessionStatus) -> Void in
            self.logger.info(
                msg: "Video primary meeting promotion has completed with status \(status.statusCode.description)")
            videoClientPromotionStatus = status
            group.leave()
        }
        let videoClientDemotionCallback = { (status: MeetingSessionStatus) -> Void in
            self.logger.info(
                msg: "Video primary meeting demotion has completed with status \(status.statusCode.description)")
            self.audioClientController.demoteFromPrimaryMeeting()
            observer.didDemoteFromPrimaryMeeting(status: status)
        }
        group.enter()
        var audioClientPromotionStatus: MeetingSessionStatus?
        let audioClientPromotionCallback = { (status: MeetingSessionStatus) -> Void in
            self.logger.info(
                msg: "Audio primary meeting promotion has completed with status \(status.statusCode.description)")
            audioClientPromotionStatus = status
            group.leave()
        }
        let audioClientDemotionCallback = { (status: MeetingSessionStatus) -> Void in
            self.logger.info(
                msg: "Audio primary meeting demotion has completed with status \(status.statusCode.description)")
            self.videoClientController.demoteFromPrimaryMeeting()
            observer.didDemoteFromPrimaryMeeting(status: status)
        }
        group.enter()

        // 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
        class PrimaryMeetingPromotionObserverAdapter: PrimaryMeetingPromotionObserver {
            var promotionCallback: (MeetingSessionStatus) -> Void
            var demotionCallback: (MeetingSessionStatus) -> Void

            init(promotionCallback: @escaping (MeetingSessionStatus) -> Void,
                demotionCallback: @escaping (MeetingSessionStatus) -> Void) {
                self.promotionCallback = promotionCallback
                self.demotionCallback = demotionCallback
            }

            func didPromoteToPrimaryMeeting(status: MeetingSessionStatus) {
                self.promotionCallback(status)
            }

            func didDemoteFromPrimaryMeeting(status: MeetingSessionStatus) {
                self.demotionCallback(status)
            }
        }
        let videoClientPromotionObserverAdapter = PrimaryMeetingPromotionObserverAdapter(
            promotionCallback: videoClientPromotionCallback,
            demotionCallback: videoClientDemotionCallback)
        let audioClientPromotionObserverAdapter = PrimaryMeetingPromotionObserverAdapter(
            promotionCallback: audioClientPromotionCallback,
            demotionCallback: audioClientDemotionCallback)
        primaryMeetingPromotionObserver = observer // Store for demotion
        videoClientController.promoteToPrimaryMeeting(credentials: credentials,
                                                      observer: videoClientPromotionObserverAdapter)
        audioClientController.promoteToPrimaryMeeting(credentials: credentials,
                                                        observer: audioClientPromotionObserverAdapter)

        group.notify(queue: DispatchQueue.main) {
            if let videoClientStatus = videoClientPromotionStatus, let audioClientStatus = audioClientPromotionStatus {
                // Mux the statuses together, single failure is total failure
                if videoClientStatus.statusCode != MeetingSessionStatusCode.ok {
                    self.audioClientController.demoteFromPrimaryMeeting() // Leave other controller
                    observer.didPromoteToPrimaryMeeting(status: videoClientStatus)
                } else if audioClientStatus.statusCode != MeetingSessionStatusCode.ok {
                    self.videoClientController.demoteFromPrimaryMeeting()  // Leave other controller
                    observer.didPromoteToPrimaryMeeting(status: audioClientStatus)
                } else {
                    observer.didPromoteToPrimaryMeeting(status: videoClientStatus) // Just use video client status
                }
            }
        }
    }

    public func demoteFromPrimaryMeeting() {
        videoClientController.demoteFromPrimaryMeeting()
        audioClientController.demoteFromPrimaryMeeting()
        primaryMeetingPromotionObserver?.didDemoteFromPrimaryMeeting(
            status: MeetingSessionStatus(statusCode: MeetingSessionStatusCode.ok))
    }
}


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

import Foundation

@objcMembers public class DefaultAudioVideoFacade: NSObject, AudioVideoFacade {
    public let configuration: MeetingSessionConfiguration
    public let logger: Logger

    let audioVideoController: AudioVideoControllerFacade
    let realtimeController: RealtimeControllerFacade
    let deviceController: DeviceController
    let videoTileController: VideoTileController
    let activeSpeakerDetector: ActiveSpeakerDetectorFacade
    let contentShareController: ContentShareController
    let eventAnalyticsController: EventAnalyticsController

    public init(
        audioVideoController: AudioVideoControllerFacade,
        realtimeController: RealtimeControllerFacade,
        deviceController: DeviceController,
        videoTileController: VideoTileController,
        activeSpeakerDetector: ActiveSpeakerDetectorFacade,
        contentShareController: ContentShareController,
        eventAnalyticsController: EventAnalyticsController,
        meetingStatsCollector: MeetingStatsCollector
    ) {
        self.audioVideoController = audioVideoController
        self.realtimeController = realtimeController
        self.deviceController = deviceController
        self.videoTileController = videoTileController
        self.configuration = audioVideoController.configuration
        self.logger = audioVideoController.logger
        self.activeSpeakerDetector = activeSpeakerDetector
        self.contentShareController = contentShareController
        self.eventAnalyticsController = eventAnalyticsController
    }

    public func start(audioVideoConfiguration: AudioVideoConfiguration) throws {
        try audioVideoController.start(audioVideoConfiguration: audioVideoConfiguration)

        trace(name: "start(audioVideoConfiguration: \(audioVideoConfiguration))")
    }

    public func start(callKitEnabled: Bool = false) throws {
        try self.start(audioVideoConfiguration: AudioVideoConfiguration(callKitEnabled: callKitEnabled))
    }

    public func start() throws {
        try self.start(audioVideoConfiguration: AudioVideoConfiguration())
    }

    public func stop() {
        audioVideoController.stop()
        trace(name: "stop")
    }

    public func startLocalVideo() throws {
        try audioVideoController.startLocalVideo()
    }

    public func startLocalVideo(config: LocalVideoConfiguration) throws {
        try audioVideoController.startLocalVideo(config: config)
    }

    public func startLocalVideo(source: VideoSource) {
        audioVideoController.startLocalVideo(source: source)
    }

    public func startLocalVideo(source: VideoSource, config: LocalVideoConfiguration) {
        audioVideoController.startLocalVideo(source: source, config: config)
    }

    public func stopLocalVideo() {
        audioVideoController.stopLocalVideo()
    }

    public func startRemoteVideo() {
        audioVideoController.startRemoteVideo()
    }

    public func stopRemoteVideo() {
        audioVideoController.stopRemoteVideo()
    }

    private func trace(name: String) {
        let message = "API/DefaultAudioVideoFacade/\(name)"
        audioVideoController.logger.info(msg: message)
    }

    // MARK: RealtimeControllerFacade

    public func realtimeLocalMute() -> Bool {
        return realtimeController.realtimeLocalMute()
    }

    public func realtimeLocalUnmute() -> Bool {
        return realtimeController.realtimeLocalUnmute()
    }

    public func addRealtimeObserver(observer: RealtimeObserver) {
        realtimeController.addRealtimeObserver(observer: observer)
    }

    public func removeRealtimeObserver(observer: RealtimeObserver) {
        realtimeController.removeRealtimeObserver(observer: observer)
    }

    public func addRealtimeDataMessageObserver(topic: String, observer: DataMessageObserver) {
        realtimeController.addRealtimeDataMessageObserver(topic: topic, observer: observer)
    }

    public func removeRealtimeDataMessageObserverFromTopic(topic: String) {
        realtimeController.removeRealtimeDataMessageObserverFromTopic(topic: topic)
    }

    public func realtimeSendDataMessage(topic: String, data: Any, lifetimeMs: Int32 = 0) throws {
        try realtimeController.realtimeSendDataMessage(topic: topic, data: data, lifetimeMs: lifetimeMs)
    }

    public func realtimeSetVoiceFocusEnabled(enabled: Bool) -> Bool {
        return realtimeController.realtimeSetVoiceFocusEnabled(enabled: enabled)
    }

    public func realtimeIsVoiceFocusEnabled() -> Bool {
        return realtimeController.realtimeIsVoiceFocusEnabled()
    }

    public func addAudioVideoObserver(observer: AudioVideoObserver) {
        audioVideoController.addAudioVideoObserver(observer: observer)
    }

    public func removeAudioVideoObserver(observer: AudioVideoObserver) {
        audioVideoController.removeAudioVideoObserver(observer: observer)
    }

    public func addMetricsObserver(observer: MetricsObserver) {
        audioVideoController.addMetricsObserver(observer: observer)
    }

    public func removeMetricsObserver(observer: MetricsObserver) {
        audioVideoController.removeMetricsObserver(observer: observer)
    }

    public func addRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {
        realtimeController.addRealtimeTranscriptEventObserver?(observer: observer)
    }

    public func removeRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {
        realtimeController.removeRealtimeTranscriptEventObserver?(observer: observer)
    }

    public func updateVideoSourceSubscriptions(addedOrUpdated: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration>, removed: Array<RemoteVideoSource>) {
        audioVideoController.updateVideoSourceSubscriptions(addedOrUpdated: addedOrUpdated, removed: removed)
    }

    public func promoteToPrimaryMeeting(credentials: MeetingSessionCredentials, observer: PrimaryMeetingPromotionObserver) {
        audioVideoController.promoteToPrimaryMeeting(credentials: credentials, observer: observer)
    }

    public func demoteFromPrimaryMeeting() {
        audioVideoController.demoteFromPrimaryMeeting()
        // Stop content share as well
        contentShareController.stopContentShare()
    }

    // MARK: DeviceController

    public func listAudioDevices() -> [MediaDevice] {
        return deviceController.listAudioDevices()
    }

    public func chooseAudioDevice(mediaDevice: MediaDevice) {
        deviceController.chooseAudioDevice(mediaDevice: mediaDevice)
    }

    public func addDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceController.addDeviceChangeObserver(observer: observer)
    }

    public func removeDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceController.removeDeviceChangeObserver(observer: observer)
    }

    public func switchCamera() {
        deviceController.switchCamera()
    }

    public func getActiveCamera() -> MediaDevice? {
        return deviceController.getActiveCamera()
    }

    public func getActiveAudioDevice() -> MediaDevice? {
        return deviceController.getActiveAudioDevice()
    }

    // MARK: VideoTileController

    public func bindVideoView(videoView: VideoRenderView, tileId: Int) {
        videoTileController.bindVideoView(videoView: videoView, tileId: tileId)
    }

    public func unbindVideoView(tileId: Int) {
        videoTileController.unbindVideoView(tileId: tileId)
    }

    public func addVideoTileObserver(observer: VideoTileObserver) {
        videoTileController.addVideoTileObserver(observer: observer)
    }

    public func removeVideoTileObserver(observer: VideoTileObserver) {
        videoTileController.removeVideoTileObserver(observer: observer)
    }

    public func pauseRemoteVideoTile(tileId: Int) {
        videoTileController.pauseRemoteVideoTile(tileId: tileId)
    }

    public func resumeRemoteVideoTile(tileId: Int) {
        videoTileController.resumeRemoteVideoTile(tileId: tileId)
    }

    // MARK: ActiveSpeakerDetector

    public func addActiveSpeakerObserver(policy: ActiveSpeakerPolicy, observer: ActiveSpeakerObserver) {
        activeSpeakerDetector.addActiveSpeakerObserver(policy: policy, observer: observer)
    }

    public func removeActiveSpeakerObserver(observer: ActiveSpeakerObserver) {
        activeSpeakerDetector.removeActiveSpeakerObserver(observer: observer)
    }

    public func hasBandwidthPriorityCallback(hasBandwidthPriority: Bool) {}

    // MARK: ContentShareController

    public func startContentShare(source: ContentShareSource) {
        contentShareController.startContentShare(source: source)
    }

    public func startContentShare(source: ContentShareSource, config: LocalVideoConfiguration) {
        contentShareController.startContentShare(source: source, config: config)
    }

    public func stopContentShare() {
        contentShareController.stopContentShare()
    }

    public func addContentShareObserver(observer: ContentShareObserver) {
        contentShareController.addContentShareObserver(observer: observer)
    }

    public func removeContentShareObserver(observer: ContentShareObserver) {
        contentShareController.removeContentShareObserver(observer: observer)
    }

    public func addEventAnalyticsObserver(observer: EventAnalyticsObserver) {
        eventAnalyticsController.addEventAnalyticsObserver(observer: observer)
    }

    public func removeEventAnalyticsObserver(observer: EventAnalyticsObserver) {
        eventAnalyticsController.removeEventAnalyticsObserver(observer: observer)
    }

    public func getMeetingHistory() -> [MeetingHistoryEvent] {
        return eventAnalyticsController.getMeetingHistory()
    }

    public func getCommonEventAttributes() -> [AnyHashable: Any] {
        return eventAnalyticsController.getCommonEventAttributes()
    }
}


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

import Foundation

/// `PrimaryMeetingPromotionObserver` handles events related to Primary meeting promotion.
/// See `AudioVideoControllerFacade.promoteToPrimaryMeeting` for more information.
@objc public protocol PrimaryMeetingPromotionObserver {
    /// Called when the `AudioVideoControllerFacade.promoteToPrimaryMeeting` completes.
    ///
    /// `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.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter status: See notes above
    func didPromoteToPrimaryMeeting(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.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter status: See notes above
    func didDemoteFromPrimaryMeeting(status: MeetingSessionStatus)
}


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

import Foundation

/// `SignalStrength` describes the signal strength of an attendee for audio.
@objc public enum SignalStrength: Int, CaseIterable, CustomStringConvertible {
    /// The attendee has no signal
    case none = 0

    /// The attendee has low signal
    case low = 1

    /// The attendee has high signal
    case high = 2

    public var description: String {
        switch self {
        case .none:
            return "none"
        case .low:
            return "low"
        case .high:
            return "high"
        }
    }
}


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

import Foundation

/// `VolumeLevel` describes the volume level of an attendee for audio.
@objc public enum VolumeLevel: Int, CaseIterable, CustomStringConvertible {
    /// The attendee is muted
    case muted = -1

    /// The attendee is not speaking
    case notSpeaking = 0

    /// The attendee is speaking at low volume
    case low = 1

    /// The attendee is speaking at medium volume
    case medium = 2

    /// The attendee is speaking at high volume
    case high = 3

    public var description: String {
        switch self {
        case .muted:
            return "muted"
        case .notSpeaking:
            return "notSpeaking"
        case .low:
            return "low"
        case .medium:
            return "medium"
        case .high:
            return "high"
        }
    }
}


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

import Foundation

/// `AudioMode` describes the audio mode in which the audio client should operate during a meeting session
@objc public enum AudioMode: Int, CaseIterable, CustomStringConvertible {
    /// The mono audio mode with single audio channel and 16KHz sampling rate, for both speaker and microphone.
    case mono16K = 1

    /// The mono audio mode with single audio channel and 48KHz sampling rate, for both speaker and microphone.
    case mono48K = 2

    /// The stereo audio mode with two audio channels for speaker, and single audio channel for microphone, both with 48KHz sampling rate.
    case stereo48K = 3

    case nodevice = 4

    public var description: String {
        switch self {
        case .mono16K:
            return "mono16K"
        case .mono48K:
            return "mono48K"
        case .stereo48K:
            return "stereo48K"
        case .nodevice:
            return "nodevice"
        }
    }
}


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

import Foundation

/// `ActiveSpeakerDetectorFacade` listens to the volume indicator updates from the `RealtimeControllerFacade`.
/// It consults the `ActiveSpeakerPolicy` to determine if the speaker is active or not.
@objc public protocol ActiveSpeakerDetectorFacade {
    /// Starts the active speaker detector on the callback for the given policy.
    ///
    /// - Parameter policy: Handles Active Speaker implementation
    /// - Parameter observer: Observer that handles Active Speaker Events
    func addActiveSpeakerObserver(
        policy: ActiveSpeakerPolicy,
        observer: ActiveSpeakerObserver
    )

    /// Stops the active speaker detector callback from being called.
    ///
    /// - Parameter observer: Observer that handles Active Speaker Events
    func removeActiveSpeakerObserver(observer: ActiveSpeakerObserver)

    /// Handles bandwidth
    ///
    /// - Parameter hasBandwidthPriority: Tells the active speaker detector
    ///     whether or not to prioritize video bandwidth for active speakers
    func hasBandwidthPriorityCallback(hasBandwidthPriority: Bool)
}


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

import Foundation

/// `ActiveSpeakerObserver` handles event related to finding active speaker and corresponding scores
@objc public protocol ActiveSpeakerObserver {
    /// Uniquely identifies this observer
    @objc var observerId: String { get }
    /// Frequency of activeSpeakerScoreDidChange
    @objc optional var scoresCallbackIntervalMs: Int { get }
    /// Observes the active speaker scores at frequency scoresCallbackIntervalMs
    ///
    /// Note: this callback will be called on main thread.
    /// Note: this callback will NOT be called unless scoresCallbackIntervalMs was set
    ///
    /// - Parameter scores: Active speaker scores for each attendee
    @objc optional func activeSpeakerScoreDidChange(scores: [AttendeeInfo: Double])
    /// Observes changes in list of active speakers
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter attendeeInfo: List of active speakers in decreasing order of score
    func activeSpeakerDidDetect(attendeeInfo: [AttendeeInfo])
}


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

import Foundation

/**
 * Implements the DefaultActiveSpeakerDetector with the `ActiveSpeakerPolicy`
 */
typealias DetectorCallback = (_ attendeeIds: [AttendeeInfo]) -> Void

@objcMembers public class DefaultActiveSpeakerDetector: NSObject, ActiveSpeakerDetectorFacade, RealtimeObserver {
    private static let activityWaitIntervalMs = 1000
    private static let activityUpdateIntervalMs = 200
    private static let millisecondsInSeconds = 1000
    private static let activityWaitIntervalInSeconds = TimeInterval(activityWaitIntervalMs / millisecondsInSeconds)

    private let speakerScores = ConcurrentDictionary<AttendeeInfo, Double>()
    private var activeSpeakers: [AttendeeInfo] = []
    private let scoresTimers = ConcurrentDictionary<String, Scheduler>()
    private var hasBandwidthPriority = false
    private let mostRecentUpdateTimestamp = ConcurrentDictionary<AttendeeInfo, TimeInterval>()
    private let selfAttendeeId: String
    private let policiesAndCallbacks = ConcurrentDictionary<String, (ActiveSpeakerPolicy, DetectorCallback)>()
    private var timer = IntervalScheduler(intervalMs: activityWaitIntervalMs, callback: {})

    public init(
        selfAttendeeId: String
    ) {
        self.selfAttendeeId = selfAttendeeId
        super.init()

        timer = IntervalScheduler(
            intervalMs: DefaultActiveSpeakerDetector.activityUpdateIntervalMs,
            callback: { [weak self] in
                guard let strongSelf = self else { return }
                strongSelf.policiesAndCallbacks.forEach { _, policyAndCallback in
                    strongSelf.speakerScores.forEach { attendeeInfo, _ in
                        let lastTimestamp = strongSelf.mostRecentUpdateTimestamp[attendeeInfo] ?? 0
                        if Date.timeIntervalSinceReferenceDate - lastTimestamp > DefaultActiveSpeakerDetector.activityWaitIntervalInSeconds {
                            strongSelf.updateScore(
                                policy: policyAndCallback.0,
                                callback: policyAndCallback.1,
                                attendeeInfo: attendeeInfo,
                                volume: VolumeLevel.notSpeaking
                            )
                        }
                    }
                }
            }
        )
        timer.start()
    }

    private func needUpdateActiveSpeakers(attendeeInfo: AttendeeInfo) -> Bool {
        if activeSpeakers.isEmpty {
            return true
        }
        return activeSpeakers.contains(attendeeInfo) != (speakerScores[attendeeInfo] != nil
            && speakerScores[attendeeInfo]! > 0.0)
    }

    private func updateActiveSpeakers(
        policy: ActiveSpeakerPolicy,
        callback: @escaping DetectorCallback,
        attendeeInfo: AttendeeInfo
    ) {
        if !needUpdateActiveSpeakers(attendeeInfo: attendeeInfo) {
            return
        }
        /// Sort speaker scores and discard zeros
        activeSpeakers = speakerScores.sorted(by: { $0.value > $1.value })
            .filter { $0.value > 0.0 }
            .map { $0.0 }
        DispatchQueue.main.async { [weak self] in
            guard let strongSelf = self else { return }
            callback(strongSelf.activeSpeakers)
        }
        let selfIsActive =
            !activeSpeakers.isEmpty && activeSpeakers[0].attendeeId == selfAttendeeId
        let hasBandwidthPriority =
            selfIsActive && policy.prioritizeVideoSendBandwidthForActiveSpeaker()
        if self.hasBandwidthPriority != hasBandwidthPriority {
            self.hasBandwidthPriority = hasBandwidthPriority
            hasBandwidthPriorityCallback(hasBandwidthPriority: hasBandwidthPriority)
        }
    }

    private func updateScore(
        policy: ActiveSpeakerPolicy,
        callback: @escaping DetectorCallback,
        attendeeInfo: AttendeeInfo,
        volume: VolumeLevel
    ) {
        let activeScore = policy.calculateScore(attendeeInfo: attendeeInfo, volume: volume)
        if speakerScores[attendeeInfo] != activeScore {
            speakerScores[attendeeInfo] = activeScore
            updateActiveSpeakers(policy: policy, callback: callback, attendeeInfo: attendeeInfo)
        }
    }

    public func hasBandwidthPriorityCallback(hasBandwidthPriority: Bool) {}

    public func volumeDidChange(volumeUpdates: [VolumeUpdate]) {
        for volumeUpdate in volumeUpdates {
            mostRecentUpdateTimestamp[volumeUpdate.attendeeInfo]
                = Date.timeIntervalSinceReferenceDate
            policiesAndCallbacks.forEach {
                updateScore(
                    policy: $0.value.0,
                    callback: $0.value.1,
                    attendeeInfo: volumeUpdate.attendeeInfo,
                    volume: volumeUpdate.volumeLevel
                )
            }
        }
    }

    public func signalStrengthDidChange(signalUpdates: [SignalUpdate]) { }

    private func removeAttendeesAndUpdate(attendeeInfo attendeeInfos: [AttendeeInfo]) {
        for attendeeInfo in attendeeInfos {
            speakerScores[attendeeInfo] = nil
            mostRecentUpdateTimestamp[attendeeInfo] = nil
            policiesAndCallbacks.forEach {
                updateActiveSpeakers(policy: $0.value.0, callback: $0.value.1, attendeeInfo: attendeeInfo)
            }
        }
    }

    public func attendeesDidLeave(attendeeInfo: [AttendeeInfo]) {
        removeAttendeesAndUpdate(attendeeInfo: attendeeInfo)
    }

    public func attendeesDidDrop(attendeeInfo: [AttendeeInfo]) {
        removeAttendeesAndUpdate(attendeeInfo: attendeeInfo)
    }

    public func attendeesDidMute(attendeeInfo: [AttendeeInfo]) { }

    public func attendeesDidUnmute(attendeeInfo: [AttendeeInfo]) { }

    public func attendeesDidJoin(attendeeInfo attendeeInfos: [AttendeeInfo]) {
        for attendeeInfo in attendeeInfos {
            speakerScores[attendeeInfo] = 0.0
            policiesAndCallbacks.forEach {
                updateActiveSpeakers(policy: $0.value.0, callback: $0.value.1, attendeeInfo: attendeeInfo)
            }
        }
    }

    public func addActiveSpeakerObserver(
        policy: ActiveSpeakerPolicy,
        observer: ActiveSpeakerObserver
    ) {
        policiesAndCallbacks[observer.observerId] = (policy, observer.activeSpeakerDidDetect)

        if observer.activeSpeakerScoreDidChange(scores:) != nil,
            let scoresCallbackIntervalMs = observer.scoresCallbackIntervalMs {
            let scoresTimer = IntervalScheduler(intervalMs: scoresCallbackIntervalMs, callback: {
                DispatchQueue.main.async { [weak self] in
                    guard let strongSelf = self else { return }
                    observer.activeSpeakerScoreDidChange?(scores: strongSelf.speakerScores.getShallowDictCopy())
                }
            })
            scoresTimer.start()
            scoresTimers[observer.observerId] = scoresTimer
        }
    }

    public func removeActiveSpeakerObserver(observer: ActiveSpeakerObserver) {
        if let scoresTimer = scoresTimers[observer.observerId] {
            scoresTimer.stop()
            scoresTimers[observer.observerId] = nil
        }

        policiesAndCallbacks[observer.observerId] = nil
    }
}


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

import Foundation

@objc public protocol ActiveSpeakerPolicy {
    /// Return the score of the speaker. If the score is 0, this speaker is not active.
    ///
    /// - Parameter attendeeInfo: Attendee to calculate the score for
    /// - Parameter volume: Volume level of the speaker
    /// - Returns: The score of the speaker. The higher score, the more active the speaker.
    func calculateScore(attendeeInfo: AttendeeInfo, volume: VolumeLevel) -> Double
    /// Indicates whether the audio video controller is allowed to increase video send bandwidth
    /// for the currently active speaker if they have an active video tile. Set this to true, if
    /// your application makes the active speaker video tile larger than the other tiles.
    ///
    /// - Returns: Whether to prioritize video bandwidth for active speakers
    func prioritizeVideoSendBandwidthForActiveSpeaker() -> Bool
}


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

import Foundation

@objcMembers public class DefaultActiveSpeakerPolicy: NSObject, ActiveSpeakerPolicy {
    public static let defaultSpeakerWeight = 0.9
    public static let defaultCutoffThreshold = 0.01
    public static let defaultTakeoverRate = 0.2

    private let volumes = ConcurrentDictionary<String, Double>()
    private let speakerWeight: Double
    private let cutoffThreshold: Double
    private let takeoverRate: Double

    convenience public override init() {
        self.init(speakerWeight: Self.defaultSpeakerWeight,
                  cutoffThreshold: Self.defaultCutoffThreshold,
                  takeoverRate: Self.defaultTakeoverRate)
    }

    public init(speakerWeight: Double = DefaultActiveSpeakerPolicy.defaultSpeakerWeight,
                cutoffThreshold: Double = DefaultActiveSpeakerPolicy.defaultCutoffThreshold,
                takeoverRate: Double = DefaultActiveSpeakerPolicy.defaultTakeoverRate) {
        self.speakerWeight = speakerWeight
        self.cutoffThreshold = cutoffThreshold
        self.takeoverRate = takeoverRate
        super.init()
    }

    public func calculateScore(attendeeInfo: AttendeeInfo, volume: VolumeLevel) -> Double {
        var scalar: Double = 1.0
        if volume == VolumeLevel.muted || volume == VolumeLevel.notSpeaking {
            scalar = 0.0
        }
        let score = (volumes[attendeeInfo.attendeeId] ?? 0.0)
            * speakerWeight
            + scalar * (1 - speakerWeight)
        volumes[attendeeInfo.attendeeId] = score
        volumes.forEach { otherAttendeeId, _ in
            if otherAttendeeId != attendeeInfo.attendeeId,
                let otherAtttendeeVolume = volumes[otherAttendeeId] {
                volumes[otherAttendeeId] = max(
                    otherAtttendeeVolume - takeoverRate * scalar,
                    0.0
                )
            }
        }
        if score < cutoffThreshold {
            return 0.0
        }
        return score
    }

    public func prioritizeVideoSendBandwidthForActiveSpeaker() -> Bool {
        return true
    }
}


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

import Foundation

private let millisPerSecond = 1000.0

/**
 * `IntervalScheduler` calls the callback every intervalMs milliseconds.
 */
@objcMembers public class IntervalScheduler: NSObject, Scheduler {
    private enum IntervalSchedulerState {
        case started
        case stopped
    }

    private let timer: DispatchSourceTimer
    private var state: IntervalSchedulerState = .stopped

    public init(intervalMs: Int, callback: @escaping () -> Void) {
        let timeInterval = TimeInterval(Double(intervalMs) / millisPerSecond)
        let timer = DispatchSource.makeTimerSource()
        timer.schedule(deadline: .now() + timeInterval, repeating: timeInterval)
        timer.setEventHandler(handler: callback)

        self.timer = timer
    }

    deinit {
        timer.setEventHandler {}
        timer.cancel()

        // Need to resume the canceled timer to avoid crash:
        // https://forums.developer.apple.com/message/46175
        start()
    }

    public func start() {
        if state == .started {
            return
        }
        state = .started
        timer.resume()
    }

    public func stop() {
        if state == .stopped {
            return
        }
        state = .stopped
        timer.suspend()
    }
}


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

import Foundation

/**
 * `Scheduler` calls a callback on the schedule determined by the implementation.
 */
@objc public protocol Scheduler {
    /**
     * Schedules the callback according to the implementation.
     */
    func start()

    /**
     * Unschedules the callback and prevents it from being called anymore.
     */
    func stop()
}


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

import Foundation

/// `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.
/// Please refer to [content share guide](https://github.com/aws/amazon-chime-sdk-ios/blob/master/guides/content_share.md) for details.
@objc public protocol ContentShareController {
    /// Start sharing the content of a given `ContentShareSource`.
    ///
    /// Once sharing has started successfully, `ContentShareObserver.contentShareDidStart` will
    /// be notified. If sharing fails or stops, `ContentShareObserver.contentShareDidStop`
    /// will be invoked with `ContentShareStatus` as the cause.
    ///
    /// This will call `VideoSource.addVideoSink(sink:)` on the provided source
    /// and `VideoSource.removeVideoSink(sink:)` on the previously provided source.
    ///
    /// Calling this function repeatedly will replace the previous `ContentShareSource` as the one being transmitted.
    ///
    /// - Parameter source: source of content to be shared
    func startContentShare(source: ContentShareSource)

    /// Start sharing the content of a given `ContentShareSource`, with configurations.
    ///
    /// Once sharing has started successfully, `ContentShareObserver.contentShareDidStart` will
    /// be notified. If sharing fails or stops, `ContentShareObserver.contentShareDidStop`
    /// will be invoked with `ContentShareStatus` as the cause.
    ///
    /// This will call `VideoSource.addVideoSink(sink:)` on the provided source
    /// and `VideoSource.removeVideoSink(sink:)` on the previously provided source.
    ///
    /// Calling this function repeatedly will replace the previous `ContentShareSource` as the one being transmitted.
    ///
    /// - Parameter source: source of content to be shared
    /// - Parameter config: configurations of emitted video stream, e.g maxBitRateKbps
    func startContentShare(source: ContentShareSource, config: LocalVideoConfiguration)

    /// Stop sharing the content of a `ContentShareSource` that previously started.
    ///
    /// Once the sharing stops successfully, `ContentShareObserver.contentShareDidStop`
    /// will be invoked with status code `ContentShareStatusCode.OK`.
    func stopContentShare()

    /// Subscribe the given observer to content share events (sharing started and stopped).
    ///
    /// - Parameter observer: observer to be notified for events
    func addContentShareObserver(observer: ContentShareObserver)

    /// Unsubscribe the given observer from content share events.
    ///
    /// - Parameter observer: observer to be removed for events
    func removeContentShareObserver(observer: ContentShareObserver)
}


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

import Foundation

/// `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.
@objc public protocol ContentShareObserver {
    /// Called when the content share has started.
    ///
    /// Note: this callback will be called on main thread.
    func contentShareDidStart()

    /// 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, stop the source after this callback is invoked.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter status: the reason why the content share has stopped
    func contentShareDidStop(status: ContentShareStatus)
}


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

import Foundation

/// `ContentShareSource` contains the media sources to attach to the content share
@objcMembers public class ContentShareSource: NSObject {
    public var videoSource: VideoSource?
}


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

import Foundation

/// `ContentShareStatus` indicates a status received regarding the content share.
@objcMembers public class ContentShareStatus: NSObject {
    public let statusCode: ContentShareStatusCode

    public init(statusCode: ContentShareStatusCode) {
        self.statusCode = statusCode
    }
}


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

import Foundation

/// `ContentShareStatusCode` indicates the reason the content share event occurred.
@objc public enum ContentShareStatusCode: Int, CustomStringConvertible {
    /// No failure.
    case ok = 0

    /// This can happen when the content share video connection is in an unrecoverable failed state.
    /// Restart content share connection when this error is encountered.
    case videoServiceFailed = 1

    public var description: String {
        switch self {
        case .ok:
            return "ok"
        case .videoServiceFailed:
            return "videoServiceFailed"
        }
    }
}


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

import AmazonChimeSDKMedia
import Foundation

@objcMembers public class DefaultContentShareController: NSObject, ContentShareController {
    private let contentShareVideoClientController: ContentShareVideoClientController

    public init(contentShareVideoClientController: ContentShareVideoClientController) {
        self.contentShareVideoClientController = contentShareVideoClientController
        super.init()
    }

    public func startContentShare(source: ContentShareSource) {
        if let videoSource = source.videoSource {
            contentShareVideoClientController.startVideoShare(source: videoSource)
        }
    }

    public func startContentShare(source: ContentShareSource, config: LocalVideoConfiguration) {
        // ignore LocalVideoConfiguration because contentshare does not have simulcast
        // we want to keep the API same as LocalVideo
        if let videoSource = source.videoSource {
            contentShareVideoClientController.startVideoShare(source: videoSource, config: config)
        }
    }

    public func stopContentShare() {
        contentShareVideoClientController.stopVideoShare()
    }

    public func addContentShareObserver(observer: ContentShareObserver) {
        contentShareVideoClientController.subscribeToVideoClientStateChange(observer: observer)
    }

    public func removeContentShareObserver(observer: ContentShareObserver) {
        contentShareVideoClientController.unsubscribeFromVideoClientStateChange(observer: observer)
    }
}


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

import Foundation
import ReplayKit

/// `InAppScreenCaptureSource` is used to share screen capture within the app. When the app is in the background,
/// there is no sample sent to handler, and screen sharing is paused. `InAppScreenCaptureSource` is only available
/// on iOS 11+ because of `RPScreenRecorder.startCapture(handler:completionHandler:)` method.
/// `InAppScreenCaptureSource` does not support rotation while it's in progress.
@available(iOS 11.0, *)
@objcMembers public class InAppScreenCaptureSource: NSObject, VideoCaptureSource {
    // This will prioritize resolution over framerate.
    public var videoContentHint: VideoContentHint = .text

    private let logger: Logger
    private let observers = ConcurrentMutableSet()
    private let sinks = ConcurrentMutableSet()

    private var screenRecorder: RPScreenRecorder {
        return RPScreenRecorder.shared()
    }
    // Use an internal source so that logic can be shared with ReplayKit broadcast sources.
    private let replayKitSource: ReplayKitSource

    public init(logger: Logger) {
        self.logger = logger
        replayKitSource = ReplayKitSource(logger: logger)
    }

    public func start() {
        if screenRecorder.isRecording {
            stop()
        }
        screenRecorder.startCapture(handler: { [weak self] sampleBuffer, sampleBufferType, error in
            guard let `self` = self else { return }
            if error != nil {
                self.logger.error(msg: "RPScreenRecorder capture error received: \(error.debugDescription)")
            } else {
                self.replayKitSource.processSampleBuffer(sampleBuffer: sampleBuffer, type: sampleBufferType)
            }
        }, completionHandler: { [weak self] error in
            guard let `self` = self else { return }
            if let error = error {
                self.logger.error(msg: "RPScreenRecorder start failed: \(error.localizedDescription)" )
                ObserverUtils.forEach(observers: self.observers) { (observer: CaptureSourceObserver) in
                    observer.captureDidFail(error: .systemFailure)
                }
            } else {
                self.logger.info(msg: "RPScreenRecorder start succeeded.")
                ObserverUtils.forEach(observers: self.observers) { (observer: CaptureSourceObserver) in
                    observer.captureDidStart()
                }
            }
        })
    }

    public func stop() {
        if !screenRecorder.isRecording {
            logger.info(msg: "RPScreenRecorder not recording, so skipping stop")
            return
        }
        screenRecorder.stopCapture { [weak self] error in
            guard let strongSelf = self else { return }
            if let error = error {
                strongSelf.logger.error(msg: "RPScreenRecorder stop failed: \(error.localizedDescription)")
                ObserverUtils.forEach(observers: strongSelf.observers) { (observer: CaptureSourceObserver) in
                    observer.captureDidFail(error: .systemFailure)
                }
            } else {
                strongSelf.logger.info(msg: "RPScreenRecorder stop succeeded.")
                ObserverUtils.forEach(observers: strongSelf.observers) { (observer: CaptureSourceObserver) in
                    observer.captureDidStop()
                }
            }
        }
    }

    public func addVideoSink(sink: VideoSink) {
        replayKitSource.addVideoSink(sink: sink)
    }

    public func removeVideoSink(sink: VideoSink) {
        replayKitSource.removeVideoSink(sink: sink)
    }

    public func addCaptureSourceObserver(observer: CaptureSourceObserver) {
        observers.add(observer)
    }

    public func removeCaptureSourceObserver(observer: CaptureSourceObserver) {
        observers.remove(observer)
    }
}


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

import Foundation
import ReplayKit

/// `ReplayKitSource` repackages `CMSampleBuffer` objects from ReplayKit into SDK
/// usable `VideoFrame` objects. It currently supports resending video frames to maintain a
/// minimum frame rate.
///
/// It does not directly contain any system library calls that actually captures the screen.
/// Builders can use `InAppScreenCaptureSource`to share screen from only their application.
/// For device level screen broadcast, take a look at the `SampleHandler` in AmazonChimeSDKDemoBroadcast.
@objcMembers public class ReplayKitSource: VideoSource {
    // This will prioritize resolution over frame rate.
    public var videoContentHint: VideoContentHint = .detail

    private let logger: Logger
    private let sinks = ConcurrentMutableSet()

    private lazy var videoFrameResender = VideoFrameResender(minFrameRate: 5) { [weak self] (frame) -> Void in
        guard let `self` = self else { return }
        self.sendVideoFrame(frame: frame)
    }

    public init(logger: Logger) {
        self.logger = logger
    }

    public func stop() {
        videoFrameResender.stop()
    }

    public func addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    public func removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    public func processSampleBuffer(sampleBuffer: CMSampleBuffer, type: RPSampleBufferType) {
        switch type {
        case .video:
            processVideo(sampleBuffer: sampleBuffer)
        case .audioApp:
            // Amazon Chime SDK does not support passing app audio yet.
            break
        case .audioMic:
            // Microphone audio is passed through the app instead of the app extension.
            break
        @unknown default:
            // Unknown sample buffer types will not be handled.
            break
        }
    }

    private func processVideo(sampleBuffer: CMSampleBuffer) {
        guard let frame = VideoFrame(sampleBuffer: sampleBuffer) else {
            logger.error(msg: "ReplayKitSource could not convert captured CMSampleBuffer to video frame")
            return
        }
        sendVideoFrame(frame: frame)
    }

    private func sendVideoFrame(frame: VideoFrame) {
        sinks.forEach { item in
            guard let sink = item as? VideoSink else { return }
            sink.onVideoFrameReceived(frame: frame)
        }
        videoFrameResender.frameDidSend(videoFrame: frame)
    }
}


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

import Foundation

@objcMembers public class AttendeeInfo: NSObject, Comparable {
    public let attendeeId: String
    public let externalUserId: String

    public init(attendeeId: String, externalUserId: String) {
        self.attendeeId = attendeeId
        self.externalUserId = externalUserId
    }

    override public func isEqual(_ object: Any?) -> Bool {
        if let other = object as? AttendeeInfo {
            return self.attendeeId == other.attendeeId
        }
        return false
    }

    override public var hash: Int {
        return attendeeId.hashValue
    }

    public static func < (lhs: AttendeeInfo, rhs: AttendeeInfo) -> Bool {
        return lhs.attendeeId < rhs.attendeeId
    }
}


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

import AmazonChimeSDKMedia
import Foundation

/// Data message received from server.
@objcMembers public class DataMessage: NSObject {
    /// Monotonically increasing server ingest time
    public let timestampMs: Int64

    /// Topic this message was sent on
    public let topic: String

    /// Data payload
    public let data: Data

    /// Sender attendee
    public let senderAttendeeId: String

    /// Sender attendee external user Id
    public let senderExternalUserId: String

    /// true if server throttled or rejected message,
    /// false if server has posted the message to its recipients or it's not a sender receipt
    public let throttled: Bool

    /// Initiailize a DataMessage object
    /// - Parameters:
    ///     - topic: The topic of this data message belongs to
    ///     - data: Data payload
    ///     - senderAttendeeId: Attendee Id
    ///     - senderExternalUserId: Attendee external user ID
    ///     - timestampMs: Monotonically increasing server ingest time
    ///     - throttled: if server throttled or rejected message

    public init(topic: String,
                data: Data,
                senderAttendeeId: String,
                senderExternalUserId: String,
                timestampMs: Int64,
                throttled: Bool) {
        self.topic = topic
        self.data = data
        self.senderAttendeeId = senderAttendeeId
        self.senderExternalUserId = senderExternalUserId
        self.timestampMs = timestampMs
        self.throttled = throttled
    }

    /// Convenience way to initialize DataMessage object with Internal DataMessage
    /// - Parameters:
    ///     - message: A Internal datamessage object
    convenience init(message: DataMessageInternal) {
        self.init(topic: message.topic,
                  data: message.data,
                  senderAttendeeId: message.senderAttendeeId,
                  senderExternalUserId: message.senderExternalUserId,
                  timestampMs: message.timestampMs,
                  throttled: Bool(message.throttled))
    }

    /// Marshal data byte array to String
    /// - Returns: utf8 encoding string of data, null if data contains non utf8 characters
    public func text() -> String? {
        return String(data: self.data, encoding: .utf8)
    }

    /// Try deserialize data byte array to swift basic collection type
    /// - Returns: null if not deserializable, or swift basic collection type
    public func fromJSON() -> Any? {
        do {
            let json = try JSONSerialization.jsonObject(with: self.data)
            return json
        } catch {
            return nil
        }
    }
}


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

import Foundation

@objcMembers public class SignalUpdate: NSObject {
    public let attendeeInfo: AttendeeInfo
    public let signalStrength: SignalStrength

    public init(attendeeInfo: AttendeeInfo, signalStrength: SignalStrength) {
        self.attendeeInfo = attendeeInfo
        self.signalStrength = signalStrength
    }
}


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

import Foundation

@objcMembers public class VolumeUpdate: NSObject {
    public let attendeeInfo: AttendeeInfo
    public let volumeLevel: VolumeLevel

    public init(attendeeInfo: AttendeeInfo, volumeLevel: VolumeLevel) {
        self.attendeeInfo = attendeeInfo
        self.volumeLevel = volumeLevel
    }
}


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

import Foundation

/// 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
@objcMembers public class Transcript: NSObject, TranscriptEvent {
    public let results: [TranscriptResult]

    public init(results: [TranscriptResult]) {
        self.results = results
    }
}


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

import Foundation

/// 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
@objcMembers public class TranscriptAlternative: NSObject {
    public let items: [TranscriptItem]
    public let entities: [TranscriptEntity]?
    public let transcript: String

    public init(items: [TranscriptItem], transcript: String, entities: [TranscriptEntity]?) {
        self.items = items
        self.entities = entities
        self.transcript = transcript
    }
}


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

import Foundation

/// 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
@objcMembers public class TranscriptEntity: NSObject {
    public let type: String
    public let content: String
    public let category: String
    public let confidence: Double?
    public let startTimeMs: Int64
    public let endTimeMs: Int64

    public init(type: String,
                content: String,
                category: String,
                confidence: Double?,
                startTimeMs: Int64,
                endTimeMs: Int64) {
        self.type = type
        self.content = content
        self.category = category
        self.confidence = confidence
        self.startTimeMs = startTimeMs
        self.endTimeMs = endTimeMs
    }
}


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

import Foundation

/// 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
@objc public protocol TranscriptEvent {}


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

import Foundation

/// 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
@objcMembers public class TranscriptionStatus: NSObject, TranscriptEvent {
    public let type: TranscriptionStatusType
    public let eventTimeMs: Int64
    public let transcriptionRegion: String
    public let transcriptionConfiguration: String
    public let message: String?

    public init(type: TranscriptionStatusType,
                eventTimeMs: Int64,
                transcriptionRegion: String,
                transcriptionConfiguration: String,
                message: String?) {
        self.type = type
        self.eventTimeMs = eventTimeMs
        self.transcriptionRegion = transcriptionRegion
        self.transcriptionConfiguration = transcriptionConfiguration
        self.message = message
    }
}


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

import Foundation

/// 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
@objc public enum TranscriptionStatusType: Int, Equatable, CustomStringConvertible {
    case unknown = 0
    case started = 1
    case interrupted = 2
    case resumed = 3
    case stopped = 4
    case failed = 5

    public var description: String {
        switch self {
        case .unknown:
            return "unknown"
        case .started:
            return "started"
        case .interrupted:
            return "interrupted"
        case .resumed:
            return "resumed"
        case .stopped:
            return "stopped"
        case .failed:
            return "failed"
        }
    }
}


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

import Foundation

/// 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
@objcMembers public class TranscriptItem: NSObject {
    public let type: TranscriptItemType
    public let startTimeMs: Int64
    public let endTimeMs: Int64
    public let attendee: AttendeeInfo
    public let content: String
    public let vocabularyFilterMatch: Bool
    public let stable: Bool?
    public let confidence: Double?

    public init(type: TranscriptItemType,
                startTimeMs: Int64,
                endTimeMs: Int64,
                attendee: AttendeeInfo,
                content: String,
                vocabularyFilterMatch: Bool,
                stable: Bool?,
                confidence: Double?) {
        self.type = type
        self.startTimeMs = startTimeMs
        self.endTimeMs = endTimeMs
        self.attendee = attendee
        self.content = content
        self.vocabularyFilterMatch = vocabularyFilterMatch
        self.stable = stable
        self.confidence = confidence
    }
}


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

import Foundation

/// 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
@objc public enum TranscriptItemType: Int, CaseIterable, CustomStringConvertible {
    case unknown = 0
    case pronunciation = 1
    case punctuation = 2

    public var description: String {
        switch self {
        case .unknown:
            return "unknown"
        case .pronunciation:
            return "pronunciation"
        case .punctuation:
            return "punctuation"
        }
    }
}


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

import Foundation

/// See [Using Amazon Chime SDK live transcription developer guide](https://docs.aws.amazon.com/transcribe/latest/dg/lang-id.html) for details about transcription message types and data guidelines
@objcMembers public class TranscriptLanguageWithScore: NSObject {
    public let languageCode: String
    public let score: Double

    public init(languageCode: String, score: Double) {
        self.languageCode = languageCode
        self.score = score
    }
}


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

import Foundation

/// 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
@objcMembers public class TranscriptResult: NSObject {
    public let resultId: String
    public let channelId: String?
    public let isPartial: Bool
    public let startTimeMs: Int64
    public let endTimeMs: Int64
    public let alternatives: [TranscriptAlternative]
    public let languageCode: String?
    public let languageIdentification: [TranscriptLanguageWithScore]?

    public init(resultId: String,
                channelId: String?,
                isPartial: Bool,
                startTimeMs: Int64,
                endTimeMs: Int64,
                alternatives: [TranscriptAlternative],
                languageCode: String?,
                languageIdentification: [TranscriptLanguageWithScore]?) {
        self.resultId = resultId
        self.channelId = channelId
        self.isPartial = isPartial
        self.startTimeMs = startTimeMs
        self.endTimeMs = endTimeMs
        self.alternatives = alternatives
        self.languageCode = languageCode
        self.languageIdentification = languageIdentification
    }
}


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

import Foundation

/// `MetricsObserver` handles events related to audio/video metrics.
@objc public protocol MetricsObserver {
    /// Called when metrics are collected and ready
    ///
    /// - Parameter metrics: A dictionary of ObservableMetric case to value
    func metricsDidReceive(metrics: [AnyHashable: Any])
}


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

import Foundation

/// `ObservableMetric` types represents filtered metrics that are intended to propagate to the
/// top level observers. All metrics are measured over the past second.
///
/// Send video metrics are only reported when sending.
/// Receive video metrics are only reported when receiving.
@objc public enum ObservableMetric: Int, CustomStringConvertible {
    /// Percentage of audio packets lost from server to client
    case audioReceivePacketLossPercent
    /// Percentage of audio packets lost from client to server
    case audioSendPacketLossPercent
    /// Estimated uplink bandwidth from perspective of video client
    case videoAvailableSendBandwidth
    /// Estimated downlink bandwidth from perspective of video client
    case videoAvailableReceiveBandwidth
    /// Sum of total bitrate across all send streams
    case videoSendBitrate
    /// Percentage of video packets lost from client to server across all send streams
    case videoSendPacketLossPercent
    /// Average send FPS across all send streams
    case videoSendFps
    /// Round trip time of packets sent from client to server
    case videoSendRttMs
    /// Sum of total bitrate across all receive streams
    case videoReceiveBitrate
    /// Percentage of video packets lost from server to client across all receive streams
    case videoReceivePacketLossPercent

    /// Below are metrics for content share stream

    /// Sum of total bitrate across all send streams
    case contentShareVideoSendBitrate
    /// Percentage of video packets lost from client to server across all send streams
    case contentShareVideoSendPacketLossPercent
    /// Average send FPS across all send streams
    case contentShareVideoSendFps
    /// Round trip time of packets sent from client to server
    case contentShareVideoSendRttMs

    public var description: String {
        switch self {
        case .audioReceivePacketLossPercent:
            return "audioReceivePacketLossPercent"
        case .audioSendPacketLossPercent:
            return "audioSendPacketLossPercent"
        case .videoAvailableSendBandwidth:
            return "videoAvailableSendBandwidth"
        case .videoAvailableReceiveBandwidth:
            return "videoAvailableReceiveBandwidth"
        case .videoSendBitrate:
            return "videoSendBitrate"
        case .videoSendPacketLossPercent:
            return "videoSendPacketLossPercent"
        case .videoSendFps:
            return "videoSendFps"
        case .videoSendRttMs:
            return "videoSendRttMs"
        case .videoReceiveBitrate:
            return "videoReceiveBitrate"
        case .videoReceivePacketLossPercent:
            return "videoReceivePacketLossPercent"
        case .contentShareVideoSendBitrate:
            return "contentShareVideoSendBitrate"
        case .contentShareVideoSendPacketLossPercent:
            return "contentShareVideoSendPacketLossPercent"
        case .contentShareVideoSendFps:
            return "contentShareVideoSendFps"
        case .contentShareVideoSendRttMs:
            return "contentShareVideoSendRttMs"
        }
    }

    /// Determine if the metric is for content share stream.
    public var isContentShareMetric: Bool {
        return description.hasPrefix("contentShare")
    }
}


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

import os
import UIKit
import VideoToolbox

@objcMembers public class DefaultVideoRenderView: UIImageView, VideoRenderView {
    private let scalingContentModes: [UIView.ContentMode] = [.scaleAspectFill, .scaleToFill, .scaleAspectFit]

    public var mirror: Bool = false {
        didSet {
            transformNeedsUpdate = true
        }
    }

    override public var contentMode: UIView.ContentMode {
        willSet(newContentMode) {
            if !scalingContentModes.contains(newContentMode) {
                os_log("""
                Recommend to use a scaling ContentMode on the VideoRenderView,
                as video resolution may change during the session.
                """, type: .info)
            }
            imageView.contentMode = newContentMode
        }
    }

    // Delay the transform until the next frame
    private var transformNeedsUpdate: Bool = false
    // We use an internal UIImageView so we can mirror
    // it without mirroring the entire view
    private var imageView: UIImageView

    public required init?(coder: NSCoder) {
        imageView = UIImageView()
        super.init(coder: coder)

        initImageView()
    }

    override public init(frame: CGRect) {
        imageView = UIImageView()
        super.init(frame: frame)

        initImageView()
    }

    public convenience init() {
        self.init(frame: .zero)
    }

    private func initImageView() {
        addSubview(imageView)
        sendSubviewToBack(imageView)
        imageView.frame = bounds
        imageView.contentMode = contentMode
        imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }

    public func onVideoFrameReceived(frame: VideoFrame) {
        if Thread.isMainThread {
            renderFrame(frame: frame)
        } else {
            DispatchQueue.main.async {
                self.renderFrame(frame: frame)
            }
        }
    }

    public func resetImage() {
        imageView.image = nil
    }

    private func renderFrame(frame: VideoFrame) {
        guard let buffer = (frame.buffer as? VideoFramePixelBuffer)?.pixelBuffer else {
            return
        }

        var cgImage: CGImage?
        VTCreateCGImageFromCVPixelBuffer(buffer, options: nil, imageOut: &cgImage)
        guard let image = cgImage else {
            return
        }

        if transformNeedsUpdate {
            transformNeedsUpdate = false
            if mirror {
                imageView.transform = CGAffineTransform(scaleX: -1, y: 1)
            } else {
                imageView.transform = CGAffineTransform(scaleX: 1, y: 1)
            }
        }
        imageView.image = UIImage(cgImage: image)
    }
}


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

import Foundation
import VideoToolbox

@objcMembers public class DefaultVideoTile: NSObject, VideoTile {
    public var state: VideoTileState

    private let logger: Logger

    public var videoRenderView: VideoRenderView?

    public init(tileId: Int,
                attendeeId: String,
                videoStreamContentWidth: Int,
                videoStreamContentHeight: Int,
                isLocalTile: Bool,
                logger: Logger) {
        state = VideoTileState(tileId: tileId,
                               attendeeId: attendeeId,
                               videoStreamContentWidth: videoStreamContentWidth,
                               videoStreamContentHeight: videoStreamContentHeight,
                               pauseState: .unpaused,
                               isLocalTile: isLocalTile)
        self.logger = logger
    }

    public func bind(videoRenderView: VideoRenderView?) {
        logger.info(
            msg: "Binding the view to tile: tileId: \(state.tileId), attendeeId: \(state.attendeeId)"
        )
        self.videoRenderView = videoRenderView
    }

    public func onVideoFrameReceived(frame: VideoFrame) {
        videoRenderView?.onVideoFrameReceived(frame: frame)
    }

    public func unbind() {
        logger.info(
            msg: "Unbinding the view from tile: tileId: \(state.tileId), attendeeId: \(state.attendeeId)"
        )
        videoRenderView = nil
    }

    public func setPauseState(pauseState: VideoPauseState) {
        state.pauseState = pauseState
    }
}


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

import Foundation
import UIKit

@objcMembers public class DefaultVideoTileController: NSObject, VideoTileController {
    private let logger: Logger
    private var videoTileMap = [Int: VideoTile]()
    private var videoViewToTileMap = [NSValue: VideoTile]()
    private let videoTileObservers = ConcurrentMutableSet()
    private let videoClientController: VideoClientController
    private let meetingStatsCollector: MeetingStatsCollector

    public init(videoClientController: VideoClientController,
                logger: Logger,
                meetingStatsCollector: MeetingStatsCollector) {
        self.videoClientController = videoClientController
        self.logger = logger
        self.meetingStatsCollector = meetingStatsCollector
    }

    public func onReceiveFrame(frame: VideoFrame?,
                               videoId: Int,
                               attendeeId: String?,
                               pauseState: VideoPauseState) {
        var videoStreamContentWidth = 0
        var videoStreamContentHeight = 0

        if let frame = frame {
            videoStreamContentWidth = frame.width
            videoStreamContentHeight = frame.height
        }

        if let videoTile = videoTileMap[videoId] {
            // when removing video track, video client will send an unpaused nil frame
            if pauseState == .unpaused && frame == nil {
                onRemoveTrack(tileState: videoTile.state)
                return
            }

            if frame != nil && (videoStreamContentWidth != videoTile.state.videoStreamContentWidth ||
                videoStreamContentHeight != videoTile.state.videoStreamContentHeight) {
                videoTile.state.videoStreamContentWidth = videoStreamContentWidth
                videoTile.state.videoStreamContentHeight = videoStreamContentHeight
                ObserverUtils.forEach(observers: videoTileObservers) { (videoTileObserver: VideoTileObserver) in
                    videoTileObserver.videoTileSizeDidChange(tileState: videoTile.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 != videoTile.state.pauseState, videoTile.state.pauseState != .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
                videoTile.setPauseState(pauseState: pauseState)
                if pauseState == .unpaused {
                    ObserverUtils.forEach(observers: videoTileObservers) { (videoTileObserver: VideoTileObserver) in
                        videoTileObserver.videoTileDidResume(tileState: videoTile.state)
                    }
                } else {
                    ObserverUtils.forEach(observers: videoTileObservers) { (videoTileObserver: VideoTileObserver) in
                        videoTileObserver.videoTileDidPause(tileState: videoTile.state)
                    }
                }
            }

            // only render unpaused non-nil frames
            if videoTile.state.pauseState == .unpaused, let frame = frame {
                videoTile.onVideoFrameReceived(frame: frame)
            }
        } else if frame != nil || pauseState != .unpaused {
            onAddTrack(tileId: videoId,
                       attendeeId: attendeeId,
                       pauseState: pauseState,
                       videoStreamContentWidth: videoStreamContentWidth,
                       videoStreamContentHeight: videoStreamContentHeight)
        }
    }

    public func bindVideoView(videoView: VideoRenderView, tileId: Int) {
        logger.info(msg: "Binding VideoView to Tile with tileId = \(tileId)")
        let videoRenderKey = NSValue(nonretainedObject: videoView)

        // Previously there was another videoTile that bounded to the videoView, unbind it
        if let matchedTile = videoViewToTileMap[videoRenderKey] {
            logger.info(msg: "Override the binding from \(matchedTile.state.tileId) to \(tileId)")
            removeVideoViewBindMapping(tileId: matchedTile.state.tileId)
        }

        if let videoTile = videoTileMap[tileId] {
            if videoTile.videoRenderView != nil {
                // If tileId was already bound to another videoRenderView, unbind it
                logger.info(msg: "tileId = \(tileId) already had a different video view. Unbinding the old one and associating the new one")
                removeVideoViewBindMapping(tileId: tileId)
            }
            videoTile.bind(videoRenderView: videoView)
            videoViewToTileMap[videoRenderKey] = videoTile
        }
    }

    private func removeVideoViewBindMapping(tileId: Int) {
        videoViewToTileMap.first(where: { $1.state.tileId == tileId }).map { videoRenderKey, videoTile in
            videoTile.unbind()
            videoViewToTileMap.removeValue(forKey: videoRenderKey)
        }
    }

    public func unbindVideoView(tileId: Int) {
        logger.info(msg: "Unbinding VideoView to Tile with tileId = \(tileId)")
        // Remove the video from both mappings when unbind, in order to keep the old SDK behavior
        videoTileMap.removeValue(forKey: tileId)
        removeVideoViewBindMapping(tileId: tileId)
    }

    private func onAddTrack(tileId: Int,
                            attendeeId: String?,
                            pauseState: VideoPauseState,
                            videoStreamContentWidth: Int,
                            videoStreamContentHeight: Int) {
        var isLocalTile: Bool
        var thisAttendeeId: String
        let selfAttendeeId = videoClientController.getConfiguration().credentials.attendeeId

        if let attendeeId = attendeeId {
            thisAttendeeId = attendeeId
            isLocalTile = false
        } else {
            thisAttendeeId = selfAttendeeId
            isLocalTile = true
        }
        let tile = DefaultVideoTile(tileId: tileId,
                                    attendeeId: thisAttendeeId,
                                    videoStreamContentWidth: videoStreamContentWidth,
                                    videoStreamContentHeight: videoStreamContentHeight,
                                    isLocalTile: isLocalTile,
                                    logger: logger)
        tile.setPauseState(pauseState: pauseState)
        videoTileMap[tileId] = tile
        self.meetingStatsCollector.updateMaxVideoTile(videoTileCount: videoTileMap.count)
        ObserverUtils.forEach(observers: videoTileObservers) { (videoTileObserver: VideoTileObserver) in
            videoTileObserver.videoTileDidAdd(tileState: tile.state)
        }
    }

    private func onRemoveTrack(tileState: VideoTileState) {
        ObserverUtils.forEach(observers: videoTileObservers) { (videoTileObserver: VideoTileObserver) in
            videoTileObserver.videoTileDidRemove(tileState: tileState)
        }
        videoTileMap.removeValue(forKey: tileState.tileId)
    }

    public func addVideoTileObserver(observer: VideoTileObserver) {
        videoTileObservers.add(observer)
    }

    public func removeVideoTileObserver(observer: VideoTileObserver) {
        videoTileObservers.remove(observer)
    }

    public func pauseRemoteVideoTile(tileId: Int) {
        if let videoTile = videoTileMap[tileId] {
            if videoTile.state.isLocalTile {
                logger.fault(msg: "You cannot pauseRemoteVideoTile on local VideoTile")
                return
            }

            logger.info(msg: "pauseRemoteVideoTile id=\(tileId)")
            videoClientController.pauseResumeRemoteVideo(UInt32(tileId), pause: true)
            // Don't update state/observers if we haven't changed anything
            // Note that this will overwrite .pausedForPoorConnection if that is the current state
            if videoTile.state.pauseState != .pausedByUserRequest {
                videoTile.setPauseState(pauseState: .pausedByUserRequest)
                ObserverUtils.forEach(observers: videoTileObservers) { (videoTileObserver: VideoTileObserver) in
                    videoTileObserver.videoTileDidPause(tileState: videoTile.state)
                }
            }
        }
    }

    public func resumeRemoteVideoTile(tileId: Int) {
        if let videoTile = videoTileMap[tileId] {
            if videoTile.state.isLocalTile {
                logger.fault(msg: "You cannot resumeRemoteVideoTile on local VideoTile")
                return
            }

            logger.info(msg: "resumeRemoteVideoTile id=\(tileId)")
            videoClientController.pauseResumeRemoteVideo(UInt32(tileId), pause: false)
            // 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 videoTile.state.pauseState == .pausedByUserRequest {
                videoTile.setPauseState(pauseState: .unpaused)
                ObserverUtils.forEach(observers: videoTileObservers) { (videoTileObserver: VideoTileObserver) in
                    videoTileObserver.videoTileDidResume(tileState: videoTile.state)
                }
            }
        }
    }
}


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

import Foundation

/// Configuration for a local video or content share to be sent
@objcMembers public class LocalVideoConfiguration: NSObject {

    /// The flag to disable/enable simulcast, default to true
    /// For local video use only, will not work for content share
    public var simulcastEnabled: Bool

    /// 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
    public var maxBitRateKbps: UInt32

    public init(maxBitRateKbps: UInt32 = 0, simulcastEnabled: Bool = true) {
        self.maxBitRateKbps = maxBitRateKbps
        self.simulcastEnabled = simulcastEnabled
        super.init()
    }
}


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

import Foundation

/// 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.
@objcMembers public class RemoteVideoSource: NSObject {
    /// - Parameters:
    ///   -attendeeId: The attendee ID this video tile belongs to. Note that screen share video will have a suffix of #content
    public var attendeeId: String = ""
}


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

import AmazonChimeSDKMedia
import Foundation

/// `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/ .
@objc public enum VideoContentHint: Int {
    /// No hint has been provided.
    case none = 0

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

    /// 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.
    case detail = 2

    /// 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.
    case text = 3

    var toInternal: VideoContentHintInternal {
        return VideoContentHintInternal(rawValue: UInt(rawValue)) ?? .none
    }

    public var description: String {
        switch self {
        case .none:
            return "none"
        case .motion:
            return "motion"
        case .detail:
            return "detail"
        case .text:
            return "text"
        }
    }
}


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

import Foundation
import CoreMedia

/// `VideoFrame` is a class which contains a `VideoFrameBuffer` and metadata necessary for transmission.
/// Typically produced via a `VideoSource` and consumed via a `VideoSink`
@objcMembers public class VideoFrame: NSObject {
    /// Width of the video frame in pixels.
    public var width: Int { return buffer.width() }

    /// Height of the video frame in pixels.
    public var height: Int { return buffer.height() }

    /// 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 framework,
    /// which will then be converted to a system monotonic clock on remote end.
    ///
    /// May be different on frames emanated from AmazonChimeSDKMedia framework.
    public let timestampNs: Int64

    /// 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`.
    public let rotation: VideoRotation

    /// Object containing actual video frame data in some form.
    public let buffer: VideoFrameBuffer

    public init(timestampNs: Int64, rotation: VideoRotation, buffer: VideoFrameBuffer) {
        self.timestampNs = timestampNs
        self.rotation = rotation
        self.buffer = buffer
    }

    public init?(sampleBuffer: CMSampleBuffer) {
        guard CMSampleBufferGetNumSamples(sampleBuffer) == 1,
            CMSampleBufferIsValid(sampleBuffer),
            CMSampleBufferDataIsReady(sampleBuffer),
            let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return nil
        }
        self.buffer = VideoFramePixelBuffer(pixelBuffer: pixelBuffer)
        self.timestampNs = Int64(CMTimeGetSeconds(CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer))
            * Double(Constants.nanosecondsPerSecond))
        self.rotation = sampleBuffer.getVideoRotation()
    }
}


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

import Foundation

/// `VideoFrameBuffer` is a buffer which contains a single video buffer's raw data.
/// Typically owned by a `VideoFrame` which includes additional metadata.
@objc public protocol VideoFrameBuffer {
    /// Width of the video frame in pixels.
    func width() -> Int

    /// Height of the video frame in pixels.
    func height() -> Int
}


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

import CoreVideo
import Foundation

/// `VideoFramePixelBuffer` is a buffer which contains a single video frame in the form of `CVPixelBuffer`.
@objcMembers public class VideoFramePixelBuffer: NSObject, VideoFrameBuffer {
    public func width() -> Int {
        return CVPixelBufferGetWidth(pixelBuffer)
    }

    public func height() -> Int {
        return CVPixelBufferGetHeight(pixelBuffer)
    }

    // Underlying pixel buffer holding video frame
    public let pixelBuffer: CVPixelBuffer

    public init(pixelBuffer: CVPixelBuffer) {
        self.pixelBuffer = pixelBuffer
    }
}


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

import Foundation

/// `VideoPauseState` describes the pause status of a video tile.
@objc public enum VideoPauseState: Int, CaseIterable, CustomStringConvertible {
    /// The video tile is not paused
    case unpaused = 0

    /// The video tile has been paused by the user, and will only be unpaused if the user requests it to resume.
    case 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.
    case pausedForPoorConnection = 2

    public var description: String {
        switch self {
        case .unpaused:
            return "unpaused"
        case .pausedByUserRequest:
            return "pausedByUserRequest"
        case .pausedForPoorConnection:
            return "pausedForPoorConnection"
        }
    }
}


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

import Foundation

/// 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.
@objc public enum VideoPriority: Int {
    case lowest = 0
    case low = 10
    case medium = 20
    case high = 30
    case highest = 40
}


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

import Foundation
import VideoToolbox

/// `VideoRenderView` is the type of VideoSink used by the `VideoTileController`
@objc public protocol VideoRenderView: VideoSink {}


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

import Foundation

/// Customizable video resolution parameters for a remote video source.
@objc public class VideoResolution: NSObject {
    public var width: Int = 0
    public var height: Int = 0

    /// Preset video resolutions.
    public static let high: VideoResolution = {
        let res = VideoResolution()
        res.width = 960
        res.height = 720
        return res
    }()
    public static let medium: VideoResolution = {
        let res = VideoResolution()
        res.width = 640
        res.height = 480
        return res
    }()
    public static let low: VideoResolution = {
        let res = VideoResolution()
        res.width = 320
        res.height = 240
        return res
    }()
}


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

import AmazonChimeSDKMedia
import Foundation

/// `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`.
@objc public enum VideoRotation: Int {
    /// Not rotated.
    case rotation0 = 0

    /// Rotated 90 degrees clockwise.
    case rotation90 = 90

    /// Rotated 180 degrees clockwise.
    case rotation180 = 180

    /// Rotated 270 degrees clockwise.
    case rotation270 = 270

    var toInternal: VideoRotationInternal {
        return VideoRotationInternal(rawValue: UInt(rawValue)) ?? .rotation0
    }

    init(internalValue: VideoRotationInternal) {
        self = VideoRotation(rawValue: Int(internalValue.rawValue)) ?? .rotation0
    }

    public var description: String {
        switch self {
        case .rotation0:
            return "rotation_0"
        case .rotation90:
            return "rotation_90"
        case .rotation180:
            return "rotation_180"
        case .rotation270:
            return "rotation_270"
        }
    }
}


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

import CoreMedia
import Foundation

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