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

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

/**
 * [VideoTileController] handles rendering/creating of new [VideoTile].
 */
interface VideoTileController : VideoTileControllerFacade {
    /**
     * Called whenever there is a new Video frame received for any of the attendee in the meeting
     *
     * @param frame: [VideoFrame] - A frame of video
     * @param videoId: [Int] - Unique id that belongs to video being transmitted
     * @param attendeeId: [String] - An id of user who is transmitting current frame
     * @param pauseState: [VideoPauseState] - Current pause state of the video being received
     */
    fun onReceiveFrame(
        frame: VideoFrame?,
        videoId: Int,
        attendeeId: String?,
        pauseState: VideoPauseState
    )
}


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

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

/**
 * [VideoTileControllerFacade] manages video tile binding, pausing, and resuming as well as subscribing
 * to video tile events by adding a [VideoTileObserver].
 */
interface VideoTileControllerFacade {
    /**
     * Binds the video rendering view to Video Tile. The view will start displaying the video frame
     * after the completion of this API.
     *
     * @param videoView: [VideoRenderView] - View to render the video. Application needs to create it
     * and pass to SDK.
     * @param tileId: [Int] - id of the tile which was passed to the application in [VideoTileObserver.onVideoTileAdded] .
     */
    fun bindVideoView(videoView: VideoRenderView, tileId: Int)

    /**
     * Unbinds the video rendering view from Video Tile. The view will stop displaying the video frame
     * after the completion of this API.
     *
     * @param tileId: [Int] - id of the tile which was passed to the application in [VideoTileObserver.onVideoTileAdded] .
     */
    fun unbindVideoView(tileId: Int)

    /**
     * Subscribe to Video Tile events with an [VideoTileObserver].
     *
     * @param observer: [VideoTileObserver] - The observer to subscribe to events with.
     */
    fun addVideoTileObserver(observer: VideoTileObserver)

    /**
     * Unsubscribes from Video Tile events by removing specified [VideoTileObserver].
     *
     * @param observer: [VideoTileObserver] - The observer to unsubscribe from events with.
     */
    fun removeVideoTileObserver(observer: VideoTileObserver)

    /**
     * Pauses the specified remote video tile. Ignores the tileId if it belongs to the local video tile.
     *
     * @param tileId: Int - The ID of the remote video tile to pause.
     */
    fun pauseRemoteVideoTile(tileId: Int)

    /**
     * Resumes the specified remote video tile. Ignores the tileId if it belongs to the local video tile.
     *
     * @param tileId: Int - The ID of the remote video tile to resume.
     */
    fun resumeRemoteVideoTile(tileId: Int)
}


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

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

interface VideoTileFactory {
    /**
     * Make a [VideoTile]
     *
     * @param tileId: Int - Tile ID
     * @param attendeeId: String? - Attendee ID
     * @param videoStreamContentWidth: Int - Width of video stream content
     * @param videoStreamContentHeight: Int - Height of video stream content
     * @param isLocalTile: Boolean: Whether the video tile is for the local attendee
     *
     * @return [VideoTile] to use with [VideoTileController]
    */
    fun makeTile(
        tileId: Int,
        attendeeId: String,
        videoStreamContentWidth: Int,
        videoStreamContentHeight: Int,
        isLocalTile: Boolean
    ): VideoTile
}


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

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

/**
 * [VideoTileObserver] handles events related to [VideoTile].
 *
 * Note: all callbacks will be called on main thread.
 */
interface VideoTileObserver {

    /**
     * Called whenever an attendee starts sharing the video.
     *
     * Note: this callback will be called on main thread.
     *
     * @param tileState: [VideoTileState] - Video tile state associated with new attendee.
     */
    fun onVideoTileAdded(tileState: VideoTileState)

    /**
     * Called whenever any attendee stops sharing the video.
     *
     * Note: this callback will be called on main thread.
     *
     * @param tileState: [VideoTileState] - Video tile state associated with attendee who is removed.
     */
    fun onVideoTileRemoved(tileState: VideoTileState)

    /**
     * Called whenever an attendee tile pauseState changes from [VideoPauseState.Unpaused].
     *
     * Note: this callback will be called on main thread.
     *
     * @param tileState: [VideoTileState] - Video tile state associated with attendee who is paused.
     */
    fun onVideoTilePaused(tileState: VideoTileState)

    /**
     * Called whenever an attendee tile pauseState changes to [VideoPauseState.Unpaused].
     *
     * Note: this callback will be called on main thread.
     *
     * @param tileState: [VideoTileState] - Video tile state associated with attendee who is resumed.
     */
    fun onVideoTileResumed(tileState: VideoTileState)

    /**
     * Called whenever a video steam content size is changed
     *
     * Note: this callback will be called on main thread.
     *
     * @param tileState: [VideoTileState] - Video tile state associated with attendee who is resumed.
     */
    fun onVideoTileSizeChanged(tileState: VideoTileState)
}


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

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

import com.amazonaws.services.chime.sdk.meetings.utils.DefaultModality
import com.amazonaws.services.chime.sdk.meetings.utils.ModalityType

/**
 * Contains properties related to the current state of the [VideoTile]
 *
 * @property tileId: Int - Unique Id associated with this tile
 * @property attendeeId: String - Attendee Id of the user associated with this tile
 * @property videoStreamContentWidth: Int - Width of video stream content
 * @property videoStreamContentHeight: Int - Height of video stream content
 * @property pauseState: VideoPauseState - The current pause state of the tile
 * @property isLocalTile: Boolean - Whether the video tile is for the local attendee
 */
data class VideoTileState(
    val tileId: Int,
    val attendeeId: String,
    var videoStreamContentWidth: Int,
    var videoStreamContentHeight: Int,
    var pauseState: VideoPauseState,
    val isLocalTile: Boolean
) {
    /**
     * Whether the video tile is from screen share
     */
    val isContent: Boolean = DefaultModality(attendeeId).hasModality(ModalityType.Content)
}


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

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

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.opengl.GLES20
import com.amazonaws.services.chime.cwt.ModelState
import com.amazonaws.services.chime.cwt.PredictResult
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameRGBABuffer
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.DefaultGlVideoFrameDrawer
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil
import com.amazonaws.services.chime.sdk.meetings.utils.GlTextureFrameBufferHelper
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.video.JniUtil
import java.nio.ByteBuffer

/**
 * [BackgroundFilterVideoFrameProcessor] Draws frames to RGBA, converts to CPU, identifies the
 * foreground person and applies filter (blur or replacement) to a video frame.
 * @param logger: [Logger] - Logger to log the data.
 * @param context: [Context] - Context to create blur and segmentation processor.
 * @param tag: [String] - Tag for logger.
 */
class BackgroundFilterVideoFrameProcessor(
    private val logger: Logger,
    context: Context,
    tag: String
) {
    // The camera capture source currently output OES texture frames, so we draw them to a frame buffer that
    // we can read to host memory from.
    private val rectDrawer = DefaultGlVideoFrameDrawer()

    // Helper which wraps an OpenGLES texture frame buffer with resize capabilities.
    private val textureFrameBuffer = GlTextureFrameBufferHelper(GLES20.GL_RGBA)

    private val TAG = tag

    private val segmentationProcessor = SegmentationProcessor(context)
    private var modelStateMsg: String? = null
    private var predictMsg: String? = null

    private var cachedWidth: Int = 0
    private var cachedHeight: Int = 0
    private lateinit var filteredByteBuffer: ByteBuffer

    private val defaultInputModelShape = ModelShape()
    private val channels = defaultInputModelShape.channels
    private val bitmapConfig = Bitmap.Config.ARGB_8888

    fun getInputBitmap(frame: VideoFrame): Bitmap {
        val rgbaData = this.getByteBufferFromInputVideoFrame(frame)
        val inputBitmap = Bitmap.createBitmap(
            frame.getRotatedWidth(),
            frame.getRotatedHeight(),
            bitmapConfig
        )
        inputBitmap.copyPixelsFromBuffer(rgbaData)
        JniUtil.nativeFreeByteBuffer(rgbaData)
        return inputBitmap
    }

    fun getByteBufferFromInputVideoFrame(frame: VideoFrame): ByteBuffer {
        // Note: This processor assumes that the incoming call will be on a valid EGL context.
        textureFrameBuffer.setSize(frame.getRotatedWidth(), frame.getRotatedHeight())
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, textureFrameBuffer.frameBufferId)

        val matrix = Matrix()
        matrix.preTranslate(0.5f, 0.5f)
        matrix.preScale(1f, -1f)
        matrix.preTranslate(-0.5f, -0.5f)

        // Note the draw call will account for any rotation, so we need to account for that in viewport width/height.
        rectDrawer.drawFrame(
            frame,
            0,
            0,
            frame.getRotatedWidth(),
            frame.getRotatedHeight(),
            matrix
        )

        // Read RGBA data to native byte buffer.
        val rgbaData =
            JniUtil.nativeAllocateByteBuffer(frame.getRotatedWidth() * frame.getRotatedHeight() * channels)
        GLES20.glReadPixels(
            0,
            0,
            frame.getRotatedWidth(),
            frame.getRotatedHeight(),
            GLES20.GL_RGBA,
            GLES20.GL_UNSIGNED_BYTE,
            rgbaData
        )
        GlUtil.checkGlError("glReadPixels")
        return rgbaData
    }

    /**
     * Scales the input bitmap as per ML model specification.
     */
    fun getScaledInputBitmap(frame: VideoFrame, inputBitmap: Bitmap): Bitmap {
        var scaledHeight = defaultInputModelShape.height
        var scaledWidth = defaultInputModelShape.width
        // Swap height and width for landscape orientation.
        if (frame.rotation.degrees % 180 == 0) {
            scaledHeight = defaultInputModelShape.width
            scaledWidth = defaultInputModelShape.height
        }
        return Bitmap.createScaledBitmap(inputBitmap, scaledWidth, scaledHeight, false)
    }

    fun getProcessedFrame(
        frame: VideoFrame,
        filteredBitmap: Bitmap?,
        rgbaData: ByteBuffer
    ): VideoFrame {
        if (filteredBitmap == null) {
            // Display original frame when there is an error getting segmentation mask and/or blurring.
            filteredByteBuffer = rgbaData
        } else {
            filteredByteBuffer =
                JniUtil.nativeAllocateByteBuffer(frame.getRotatedWidth() * frame.getRotatedHeight() * channels)
            filteredBitmap.copyPixelsToBuffer(filteredByteBuffer)
            filteredByteBuffer.position(0)
        }
        val rgbaBuffer =
            VideoFrameRGBABuffer(
                frame.getRotatedWidth(),
                frame.getRotatedHeight(),
                filteredByteBuffer,
                frame.getRotatedWidth() * channels,
                Runnable { JniUtil.nativeFreeByteBuffer(filteredByteBuffer) })
        return VideoFrame(frame.timestampNs, rgbaBuffer)
    }

    fun getSegmentationMask(
        scaledInputBitmap: Bitmap
    ): Bitmap? {
        // Load model only when previous frame dimensions does not match new frame dimensions.
        if (scaledInputBitmap.width != cachedWidth || scaledInputBitmap.height != cachedHeight) {
            cachedWidth = scaledInputBitmap.width
            cachedHeight = scaledInputBitmap.height
            // Load model.
            segmentationProcessor.initialize(
                scaledInputBitmap.width,
                scaledInputBitmap.height,
                defaultInputModelShape
            )
            val modelState = segmentationProcessor.modelState
            // Logs the error or info message only the first time model is loaded to avoid exploding logs for every frame.
            if (modelState != ModelState.LOADING && modelState != ModelState.LOADED && modelStateMsg == null) {
                modelStateMsg = "Failed to load model with model state $modelState"
                modelStateMsg?.let {
                    logger.error(TAG, it)
                }
            } else {
                if (modelStateMsg == null) {
                    modelStateMsg = "Model State $modelState"
                    modelStateMsg?.let {
                        logger.info(TAG, it)
                    }
                }
            }
        }

        // Set input for model.
        val inputBuffer: ByteBuffer = segmentationProcessor.getInputBuffer()
        scaledInputBitmap.copyPixelsToBuffer(inputBuffer)

        // Predict the result of model.
        val predictResult = segmentationProcessor.predict()
        // Logs the error message only the first time predict is called to avoid exploding logs for every frame.
        if (predictResult == PredictResult.ERROR && predictMsg == null) {
            predictMsg = "Error segmenting background from foreground"
            predictMsg?.let {
                logger.error(TAG, it)
            }
        }

        // Set output after prediction.
        val outputBuffer: ByteBuffer = segmentationProcessor.getOutputBuffer()
        val outputBitmap =
            Bitmap.createBitmap(scaledInputBitmap.width, scaledInputBitmap.height, bitmapConfig)
        outputBitmap.copyPixelsFromBuffer(outputBuffer)
        return outputBitmap
    }

    fun drawImageWithMask(
        scaledInputBitmap: Bitmap,
        outputBitmap: Bitmap?,
        filteredBitmap: Bitmap?
    ): Bitmap? {
        val paint = Paint()
        var paintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_IN
        paint.xfermode = PorterDuffXfermode(paintMode)
        val canvas = outputBitmap?.let { Canvas(it) }
        // Draw mask.
        canvas?.drawBitmap(scaledInputBitmap, 0.0f, 0.0f, paint)
        paintMode = PorterDuff.Mode.DST_OVER
        paint.xfermode = PorterDuffXfermode(paintMode)
        if (filteredBitmap != null) {
            canvas?.drawBitmap(filteredBitmap, 0.0f, 0.0f, paint)
        }
        return outputBitmap
    }

    fun release() {
        rectDrawer.release()
        textureFrameBuffer.release()
    }
}


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

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

/**
 * [ModelShape] Defines the shape of an ML model. This can be used to define the input and
 * output shape of an ML model.
 * @param height: [Int] The number of pixels associated with the height of the input image, defaults to 256.
 * @param width: [Int] The number of pixels associated with the width of the input image, defaults to 144.
 * @param modelRangeMin: [Int] The minimum value associated with the model output, defaults to 0.
 * @param modelRangeMax: [Int] The maximum value associated with the model output, defaults to 1.
 * @param channels: [Int] The number of channels associated with the pixels, defaults to 4 (RGBA).
 */
class ModelShape @JvmOverloads constructor(
    var height: Int = 256,
    var width: Int = 144,
    var modelRangeMin: Int = 0,
    var modelRangeMax: Int = 1,
    var channels: Int = 4
)


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

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

import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import com.amazonaws.services.chime.cwt.InputModelConfig
import com.amazonaws.services.chime.cwt.ModelState
import com.amazonaws.services.chime.cwt.PredictResult
import com.amazonaws.services.chime.cwt.TfLiteModel
import java.nio.ByteBuffer

/**
 * [SegmentationProcessor] predicts foreground mask for an image.
 */
class SegmentationProcessor(val context: Context) {
    private lateinit var segmentationModel: TfLiteModel
    lateinit var modelState: ModelState
    private lateinit var bytes: ByteArray

    /**
     * Initialize and load the tensorflow model.
     */
    fun initialize(width: Int, height: Int, modelShape: ModelShape) {
        if (isMachineLearningLibraryLoaded) {
            segmentationModel = TfLiteModel()
            bytes = context.assets.open("selfie_segmentation_landscape.tflite").readBytes()
            val config = InputModelConfig(height, width, modelShape.channels, modelShape.modelRangeMin, modelShape.modelRangeMax)
            modelState = segmentationModel.loadModelBytes(bytes, config)
        }
    }

    /**
     * Predicts the foreground on the input image.
     *
     * @return [PredictResult] value of type int.
     */
    fun predict(): PredictResult {
        return segmentationModel.predict()
    }

    /**
     * Retrieve input image on which segmentation can be applied.
     */
    fun getInputBuffer(): ByteBuffer {
        return segmentationModel.getInputBuffer()
    }

    /**
     * Retrieve segmented image mask.
     */
    fun getOutputBuffer(): ByteBuffer {
        return segmentationModel.getOutputBuffer()
    }

    @SuppressLint("LongLogTag")
    companion object {
        var isMachineLearningLibraryLoaded = false
        init {
            try {
                System.loadLibrary("amazon-chime-sdk-machine-learning_jni")
                isMachineLearningLibraryLoaded = true
            } catch (e: UnsatisfiedLinkError) {
                Log.e(
                    "BackgroundFilterVideoFrameProcessor",
                    "Unable to load Machine Learning Library. " +
                            "Use `implementation(name: 'amazon-chime-sdk-machine-learning', ext: 'aar')`" +
                            " under dependencies in build.gradle file."
                )
            }
        }
    }
}


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

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

/**
 * A set of options that can be supplied when creating a background blur video frame processor.
 * @param blurStrength: [Float] - Supported range 0 < blurStrength <= 25.
 * See https://developer.android.com/reference/android/renderscript/ScriptIntrinsicBlur#setRadius(float).
 * Defaults to value of 7.0f.
 */
class BackgroundBlurConfiguration @JvmOverloads constructor(var blurStrength: Float = 7.0f)


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

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

import android.content.Context
import android.graphics.Bitmap
import android.opengl.EGL14
import android.os.Handler
import android.os.HandlerThread
import android.renderscript.RenderScript
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.BackgroundFilterVideoFrameProcessor
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCore
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ConcurrentSet
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.video.JniUtil
import java.security.InvalidParameterException

/**
 * [BackgroundBlurVideoFrameProcessor] Draws frames to RGBA, converts to CPU, identifies the
 * foreground person and blurs the background of a video frame.
 * @param logger: [Logger] - Logger to log the data.
 * @param eglCoreFactory: [EglCoreFactory] - Factory to create [EglCore] objects to hold EGL state.
 * @param context: [Context] - Context to create blur and segmentation processor.
 * @param configurations: [BackgroundBlurConfiguration] - BlurStrength - how much blur to apply to a frame. It
 * specifies blurValue that corresponds to blur radius used in gaussian blur. It
 * accepts a float value from 0 to 25, where higher the number more blurrier the image will be.
 */
class BackgroundBlurVideoFrameProcessor @JvmOverloads constructor(
    private val logger: Logger,
    private val eglCoreFactory: EglCoreFactory,
    context: Context,
    var configurations: BackgroundBlurConfiguration?
) : VideoSource, VideoSink {
    override val contentHint: VideoContentHint = VideoContentHint.Motion

    private val BLUR_MIN_SUPPORTED_RADIUS = 0.0f
    private val BLUR_MAX_SUPPORTED_RADIUS = 25.0f
    private val TAG = "BackgroundBlurVideoFrameProcessor"
    private val DUMMY_PBUFFER_OFFSET = 0

    private lateinit var eglCore: EglCore
    private val thread: HandlerThread = HandlerThread(TAG)
    private val handler: Handler

    private val sinks = ConcurrentSet.createConcurrentSet<VideoSink>()

    private val backgroundFilterVideoFrameProcessor = BackgroundFilterVideoFrameProcessor(
        logger,
        context,
        TAG
    )

    private val blurProcessor = BlurProcessor(RenderScript.create(context))

    init {
        validateConfigs()
        thread.start()
        handler = Handler(thread.looper)

        handler.post {
            eglCore = eglCoreFactory.createEglCore()

            // We need to create a dummy surface before we can set the context as current.
            val surfaceAttribs =
                intArrayOf(EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE)
            eglCore.eglSurface = EGL14.eglCreatePbufferSurface(
                eglCore.eglDisplay,
                eglCore.eglConfig,
                surfaceAttribs,
                DUMMY_PBUFFER_OFFSET
            )
            EGL14.eglMakeCurrent(
                eglCore.eglDisplay,
                eglCore.eglSurface,
                eglCore.eglSurface,
                eglCore.eglContext
            )
            GlUtil.checkGlError("Failed to set dummy surface to initialize surface texture video source")

            logger.info(TAG, "Created $TAG")
        }
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        frame.retain()
        handler.post {
            val rgbaData =
                backgroundFilterVideoFrameProcessor.getByteBufferFromInputVideoFrame(frame)
            val inputBitmap = backgroundFilterVideoFrameProcessor.getInputBitmap(frame)

            val backgroundBlurredBitmap = getBackgroundBlurredBitmap(inputBitmap, frame)

            // Video frame with blurred background.
            val processedFrame = backgroundFilterVideoFrameProcessor.getProcessedFrame(
                frame,
                backgroundBlurredBitmap,
                rgbaData
            )
            frame.release()

            sinks.forEach { it.onVideoFrameReceived(processedFrame) }
            processedFrame.release()
            JniUtil.nativeFreeByteBuffer(rgbaData)
        }
    }

    fun getBackgroundBlurredBitmap(inputBitmap: Bitmap, frame: VideoFrame): Bitmap? {
        val width = frame.getRotatedWidth()
        val height = frame.getRotatedHeight()
        val scaleFactor = 3

        val scaledInputBitmap =
            backgroundFilterVideoFrameProcessor.getScaledInputBitmap(frame, inputBitmap)
        val maskedBitmap =
            backgroundFilterVideoFrameProcessor.getSegmentationMask(scaledInputBitmap)
        // When creating scaled bitmap, we set filter to true as it provides better image quality with smooth edges around persons boundary.
        val upScaledMaskedBitmap = maskedBitmap?.let { Bitmap.createScaledBitmap(it, width, height, true) }

        val downScaledInputBitmap = Bitmap.createScaledBitmap(inputBitmap, width / scaleFactor, height / scaleFactor, false)

        // Blur image.
        configurations?.let {
            blurProcessor.initialize(
                downScaledInputBitmap.width,
                downScaledInputBitmap.height,
                it.blurStrength
            )
        }
        val blurredBitmap = blurProcessor.process(downScaledInputBitmap)
        // When creating scaled bitmap, we set filter to false as it does not add much value to blurred image quality and decreases performance when enabled
        val upScaledBlurredBitmap = blurredBitmap?.let { Bitmap.createScaledBitmap(it, width, height, false) }

        return backgroundFilterVideoFrameProcessor.drawImageWithMask(
            inputBitmap,
            upScaledMaskedBitmap,
            upScaledBlurredBitmap
        )
    }

    override fun addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    override fun removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    private fun validateConfigs() {
        if (configurations == null) {
            configurations = BackgroundBlurConfiguration()
        }
        configurations?.blurStrength?.let {
            if (it <= BLUR_MIN_SUPPORTED_RADIUS || it > BLUR_MAX_SUPPORTED_RADIUS) {
                val msg =
                    "Blur Strength should be in the range ($BLUR_MIN_SUPPORTED_RADIUS < blurStrength <= $BLUR_MAX_SUPPORTED_RADIUS)." +
                            "See https://developer.android.com/reference/android/renderscript/ScriptIntrinsicBlur#setRadius(float) for reference."
                throw InvalidParameterException(msg)
            }
        }
    }

    fun release() {
        handler.post {
            logger.info(TAG, "Releasing $TAG source")
            blurProcessor.release()
            backgroundFilterVideoFrameProcessor.release()
            eglCore.release()

            handler.looper.quit()
        }
    }
}


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

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

import android.graphics.Bitmap
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import android.renderscript.Type
import java.lang.Exception

/**
 * [BlurProcessor] Creates input and output allocations and runs gaussian blur on each element of
 * the bitmap.
 * @param rs: [RenderScript] - RenderScript instance used to create Gaussian image blur script.
 */
internal class BlurProcessor(rs: RenderScript) {
    private val rs: RenderScript = rs
    private var inAllocation: Allocation? = null
    private var outAllocation: Allocation? = null
    private var width = 0
    private var height = 0

    // Gaussian image blur script.
    private var blurScript: ScriptIntrinsicBlur? = null

    fun initialize(width: Int, height: Int, blurStrength: Float) {
        this.width = width
        this.height = height
        blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
        blurScript?.setRadius(blurStrength) // Range 0 < radius <= 25.
        if (outAllocation != null) {
            outAllocation?.destroy()
            outAllocation = null
        }

        // Bitmap must have ARGB_8888 config for this type.
        val bitmapType = Type.Builder(rs, Element.RGBA_8888(rs))
            .setX(width)
            .setY(height)
            .setMipmaps(false)
            .create()

        // Create output allocation.
        outAllocation = Allocation.createTyped(rs, bitmapType)

        // Create input allocation with same type as output allocation.
        inAllocation = Allocation.createTyped(rs, bitmapType)
    }

    fun release() {
        blurScript?.destroy()
        blurScript = null

        inAllocation?.destroy()
        inAllocation = null

        outAllocation?.destroy()
        outAllocation = null
    }

    /**
     * Apply blur effect on each element inside an allocation.
     */
    fun process(bitmap: Bitmap): Bitmap? {
        val bitmapConfig = Bitmap.Config.ARGB_8888
        if (!bitmap.config.equals(bitmapConfig)) {
            throw Exception("Bitmap must have ARGB_8888 config.")
        }
        // Copy data from bitmap to input allocations.
        inAllocation?.copyFrom(bitmap)

        // Set input for blur script.
        blurScript?.setInput(inAllocation)

        // Process and set data to the output allocation.
        blurScript?.forEach(outAllocation)
        val blurredBitmap = Bitmap.createBitmap(this.width, this.height, bitmapConfig)
        outAllocation?.copyTo(blurredBitmap)
        return blurredBitmap
    }
}


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

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

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Shader

/**
 * A set of options that can be supplied when creating a background replacement video frame processor.
 * @param image: [Bitmap] - An image to replace video background with. Defaults to shaded blue
 * colored image.
 */
class BackgroundReplacementConfiguration @JvmOverloads constructor(var image: Bitmap = createReplacementImage())

private fun createReplacementImage(): Bitmap {
    val width = 250.0f
    val height = 250.0f
    val darkBlueColorString = "#000428"
    val lightBlueColorString = "#004e92"
    val replacementBitmap =
        Bitmap.createBitmap(width.toInt(), height.toInt(), Bitmap.Config.ARGB_8888)
    val canvas = Canvas(replacementBitmap)
    val paint = Paint()
    val startColor = Color.parseColor(darkBlueColorString)
    val endColor = Color.parseColor(lightBlueColorString)
    val linearGradient =
        LinearGradient(0.0f, 0.0f, 100.0f, 0.0f, startColor, endColor, Shader.TileMode.CLAMP)
    paint.shader = linearGradient
    canvas.drawRect(0.0f, 0.0f, width, height, paint)
    return replacementBitmap
}


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

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

import android.content.Context
import android.graphics.Bitmap
import android.opengl.EGL14
import android.os.Handler
import android.os.HandlerThread
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.BackgroundFilterVideoFrameProcessor
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCore
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ConcurrentSet
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.video.JniUtil

/**
 * [BackgroundReplacementVideoFrameProcessor] Draws frames to RGBA, converts to CPU, identifies the foreground person
 * and replaces the background of a video frame.
 * @param logger: [Logger] - Logger to log the data.
 * @param eglCoreFactory: [EglCoreFactory] - Factory to create [EglCore] objects to hold EGL state.
 * @param context: [Context] - Context to create segmentation processor.
 * @param configurations: [BackgroundReplacementConfiguration] - Image to replace the background with.
 */
class BackgroundReplacementVideoFrameProcessor @JvmOverloads constructor(
    private val logger: Logger,
    eglCoreFactory: EglCoreFactory,
    private val context: Context,
    var configurations: BackgroundReplacementConfiguration?
) : VideoSource, VideoSink {
    override val contentHint: VideoContentHint = VideoContentHint.Motion

    private val TAG = "BackgroundReplacementVideoFrameProcessor"
    private val DUMMY_PBUFFER_OFFSET = 0

    private lateinit var eglCore: EglCore
    private val thread: HandlerThread = HandlerThread(TAG)
    private val handler: Handler

    private val sinks = ConcurrentSet.createConcurrentSet<VideoSink>()

    private val backgroundFilterVideoFrameProcessor = BackgroundFilterVideoFrameProcessor(
        logger,
        context,
        TAG
    )

    init {
        validateConfigs()
        thread.start()
        handler = Handler(thread.looper)

        handler.post {
            eglCore = eglCoreFactory.createEglCore()

            // We need to create a dummy surface before we can set the context as current.
            val surfaceAttribs = intArrayOf(EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE)
            eglCore.eglSurface = EGL14.eglCreatePbufferSurface(
                eglCore.eglDisplay,
                eglCore.eglConfig,
                surfaceAttribs,
                DUMMY_PBUFFER_OFFSET
            )
            EGL14.eglMakeCurrent(
                eglCore.eglDisplay,
                eglCore.eglSurface,
                eglCore.eglSurface,
                eglCore.eglContext
            )
            GlUtil.checkGlError("Failed to set dummy surface to initialize surface texture video source")

            logger.info(TAG, "Created $TAG")
        }
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        frame.retain()
        handler.post {
            val rgbaData =
                backgroundFilterVideoFrameProcessor.getByteBufferFromInputVideoFrame(frame)
            val inputBitmap = backgroundFilterVideoFrameProcessor.getInputBitmap(frame)
            val backgroundReplacedBitmap = getBackgroundReplacedBitmap(inputBitmap, frame)

            // Video frame with replaced background.
            val processedFrame = backgroundFilterVideoFrameProcessor.getProcessedFrame(
                frame,
                backgroundReplacedBitmap,
                rgbaData
            )
            frame.release()

            sinks.forEach { it.onVideoFrameReceived(processedFrame) }
            processedFrame.release()
            JniUtil.nativeFreeByteBuffer(rgbaData)
        }
    }

    fun getBackgroundReplacedBitmap(inputBitmap: Bitmap, frame: VideoFrame): Bitmap? {
        val scaledInputBitmap =
            backgroundFilterVideoFrameProcessor.getScaledInputBitmap(frame, inputBitmap)
        val maskedBitmap =
            backgroundFilterVideoFrameProcessor.getSegmentationMask(scaledInputBitmap)
        // When creating scaled bitmap, we set filter to true as it provides better image quality with smooth edges around persons boundary.
        val upScaledMaskedBitmap = maskedBitmap?.let { Bitmap.createScaledBitmap(it, frame.getRotatedWidth(), frame.getRotatedHeight(), true) }

        val replacementBitmap = configurations?.image?.let {
            Bitmap.createScaledBitmap(
                it,
                frame.getRotatedWidth(),
                frame.getRotatedHeight(),
                false
            )
        }

        return backgroundFilterVideoFrameProcessor.drawImageWithMask(
            inputBitmap,
            upScaledMaskedBitmap,
            replacementBitmap
        )
    }

    override fun addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    override fun removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    private fun validateConfigs() {
        if (configurations == null) {
            configurations = BackgroundReplacementConfiguration()
        }
    }

    fun release() {
        handler.post {
            logger.info(TAG, "Releasing $TAG source")

            backgroundFilterVideoFrameProcessor.release()
            eglCore.release()

            handler.looper.quit()
        }
    }
}


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

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

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

/**
 * [VideoFrameBuffer] is a buffer which contains a single video buffer's raw data.
 * Typically owned by a [VideoFrame] which includes additional metadata.
 */
interface VideoFrameBuffer {
    /**
     * Width of the video frame buffer
     */
    val width: Int

    /**
     * Height of the video frame buffer
     */
    val height: Int

    /**
     * Retain the video frame buffer. Use when shared ownership of the buffer
     * is desired (e.g. when passing to separate thread), otherwise the frame
     * may be spuriously released
     */
    fun retain()

    /**
     * Release the video frame buffer. Use after frame construction or [release]
     * after the frame is no longer needed. Will trigger appropriate release of
     * any internally allocated resources. Not using may result in leaks.
     */
    fun release()
}


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

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

import com.amazonaws.services.chime.sdk.meetings.utils.RefCountDelegate
import java.nio.ByteBuffer

/**
 * [VideoFrameI420Buffer] provides an reference counted wrapper of
 * a YUV where planes are natively (i.e. in JNI) allocated direct byte buffers.
 */
class VideoFrameI420Buffer(
    override val width: Int,
    override val height: Int,

    /**
     * Y plane data of video frame in memory. This must be a natively allocated direct byte
     * buffer that it can be passed to native code
     */
    val dataY: ByteBuffer,

    /**
     * U plane data of video frame in memory. This must be a natively allocated direct byte
     * buffer so that it can be passed to native code
     */
    val dataU: ByteBuffer,

    /**
     * V plane data of video frame in memory. This must be a natively allocated direct byte
     * buffer so that it can be passed to native code
     */
    val dataV: ByteBuffer,

    /**
     * Stride of Y plane of video frame
     */
    val strideY: Int,

    /**
     * Stride of U plane of video frame
     */
    val strideU: Int,

    /**
     * Stride of V plane of video frame
     */
    val strideV: Int,

    /**
     * Callback to trigger when reference count of this buffer reaches 0 (starts as 1).
     * Use this to release underlying natively allocated direct byte buffer(s)
     */
    releaseCallback: Runnable
) : VideoFrameBuffer {
    private val refCountDelegate = RefCountDelegate(releaseCallback)

    init {
        check(dataY.isDirect && dataU.isDirect && dataV.isDirect) { "Only direct byte buffers are supported" }
    }

    override fun retain() = refCountDelegate.retain()
    override fun release() = refCountDelegate.release()
}


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

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

import com.amazonaws.services.chime.sdk.meetings.utils.RefCountDelegate
import java.nio.ByteBuffer

/**
 * [VideoFrameRGBABuffer] provides an reference counted wrapper of
 * an RGBA natively (i.e. in JNI) allocated direct byte buffer.
 */
class VideoFrameRGBABuffer(
    override val width: Int,
    override val height: Int,

    /**
     * RGBA plane data of video frame in memory. This must be a natively allocated direct byte
     * buffer so that it can be passed to native code
     */
    val data: ByteBuffer,

    /**
     * Stride of RGBA plane of video frame
     */
    val stride: Int,

    /**
     * Callback to trigger when reference count of this buffer reaches 0 (starts as 1).
     * Use this to release underlying natively allocated direct byte buffer
     */
    releaseCallback: Runnable
) : VideoFrameBuffer {
    private val refCountDelegate = RefCountDelegate(releaseCallback)

    init {
        check(data.isDirect) { "Only direct byte buffers are supported" }
    }

    override fun retain() = refCountDelegate.retain()
    override fun release() = refCountDelegate.release()
}


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

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

import android.graphics.Matrix
import android.opengl.GLES11Ext
import android.opengl.GLES20
import com.amazonaws.services.chime.sdk.meetings.utils.RefCountDelegate
import kotlinx.coroutines.Runnable

/**
 * [VideoFrameTextureBuffer] provides an reference counted wrapper of
 * an OpenGLES texture and related metadata
 */
class VideoFrameTextureBuffer(
    override val width: Int,
    override val height: Int,

    /**
     * ID of underlying GL texture
     */
    val textureId: Int,

    /**
     * The transform matrix associated with the frame. This transform matrix maps 2D
     * homogeneous coordinates of the form (s, t, 1) with s and t in the inclusive range `[0, 1]` to
     * the coordinate that should be used to sample that location from the buffer.
     */
    val transformMatrix: Matrix?,

    /**
     * GL type of underlying GL texture
     */
    val type: Type,

    /**
     * Callback to trigger when reference count of this buffer reaches 0 (starts as 1).
     * Use this to trigger capturing or processing of next frame, or to finish a release
     * which was delayed until after all in-flight frames where returned.
     */
    releaseCallback: Runnable
) : VideoFrameBuffer {
    /**
     * Wrapper enum of underlying supported GL texture types
     *
     * @param glTarget: [Int] - Underlying OpenGLES type
     */
    enum class Type(private val glTarget: Int) {
        TEXTURE_OES(GLES11Ext.GL_TEXTURE_EXTERNAL_OES),
        TEXTURE_2D(GLES20.GL_TEXTURE_2D);
    }

    private val refCountDelegate = RefCountDelegate(releaseCallback)
    override fun retain() = refCountDelegate.retain()
    override fun release() = refCountDelegate.release()
}


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

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

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

/**
 * [CameraCaptureSource] is an interface for camera capture sources with additional features
 * not covered by [VideoCaptureSource].
 *
 * All the APIs here can be called regardless of whether the [AudioVideoFacade] is started or not.
 */
interface CameraCaptureSource : VideoCaptureSource {
    /**
     * Current camera device. This is only null if the phone/device doesn't have any cameras
     * May be called regardless of whether [start] or [stop] has been called.
     */
    var device: MediaDevice?

    /**
     * Toggle for torch on the current device. Will succeed if current device has access to
     * flashlight, otherwise will stay false. May be called regardless of whether [start] or [stop]
     * has been called.
     */
    var torchEnabled: Boolean

    /**
     * Current camera capture format. Actual format may be adjusted to use supported camera formats.
     * May be called regardless of whether [start] or [stop] has been called.
     */
    var format: VideoCaptureFormat

    /**
     * Helper function to switch from front to back cameras or reverse. This also switches from
     * any external cameras to the front camera.
     */
    fun switchCamera()
}


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

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

/**
 * [CaptureSourceError] describes an error resulting from a capture source failure
 * These can be used to trigger UI, or attempt to restart the capture source.
 */
enum class CaptureSourceError {
    /**
     * Unknown error, and catch-all for errors not otherwise covered
     */
    Unknown,

    /**
     * A failure to obtain necessary permission to start video
     */
    PermissionError,

    /**
     * A failure observed from a system API used for capturing
     * e.g. In response to a `CameraDevice.StateCallback().onError` call
     */
    SystemFailure,

    /**
     * A failure observed during configuration
     */
    ConfigurationFailure;
}


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

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

/**
 * [CaptureSourceObserver] observes events resulting from different types of capture devices. Builders
 * may desire this input to decide when to show certain UI elements, or to notify users of failure.
 */
interface CaptureSourceObserver {
    /**
     * Called when the capture source has started successfully and has started emitting frames
     */
    fun onCaptureStarted()

    /**
     * Called when the capture source has stopped when expected. The capture may be in the processed of restarting,
     * e.g. this may occur when switching cameras.
     */
    fun onCaptureStopped()

    /**
     * Called when the capture source failed unexpectedly. This may be due to misconfiguration
     * or Android system error, and the capture source may be in an unknown state.
     *
     * This does not necessarily indicate that calling [VideoCaptureSource.start] again will fail.
     *
     * @param error: [CaptureSourceError] - The reason why the source has stopped.
     */
    fun onCaptureFailed(error: CaptureSourceError)
}


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

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

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Matrix
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Range
import android.view.Surface
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName
import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingHistoryEventName
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoRotation
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameBuffer
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameTextureBuffer
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice
import com.amazonaws.services.chime.sdk.meetings.device.MediaDeviceType
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ConcurrentSet
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ObserverUtils
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import kotlin.math.abs
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking

/**
 * [DefaultCameraCaptureSource] will configure a reasonably standard capture stream which will
 * use the [Surface] provided by the capture source provided by a [SurfaceTextureCaptureSourceFactory]
 */
class DefaultCameraCaptureSource @JvmOverloads constructor(
    private val context: Context,
    private val logger: Logger,
    private val surfaceTextureCaptureSourceFactory: SurfaceTextureCaptureSourceFactory,
    private val cameraManager: CameraManager = context.getSystemService(
        Context.CAMERA_SERVICE
    ) as CameraManager
) : CameraCaptureSource, VideoSink {
    private val handler: Handler

    // Camera2 system library related state
    private var cameraCaptureSession: CameraCaptureSession? = null
    private var cameraDevice: CameraDevice? = null
    private var cameraCharacteristics: CameraCharacteristics? = null

    // The following are stored from cameraCharacteristics for reuse without additional query
    // From CameraCharacteristics.SENSOR_ORIENTATION, degrees clockwise rotation
    private var sensorOrientation = 0

    // From CameraCharacteristics.LENS_FACING
    private var isCameraFrontFacing = false

    // This source provides a surface we pass into the system APIs
    // and then starts emitting frames once the system starts drawing to the
    // surface. To speed up restart, since theses sources have to wait on
    // in-flight frames to finish release, we just begin the release and
    // create a new one
    private var surfaceTextureSource: SurfaceTextureCaptureSource? = null

    private val observers = ConcurrentSet.createConcurrentSet<CaptureSourceObserver>()

    // Concurrency modification could happen when sink gets
    // added/removed from another thread while sending frames
    private val sinks = ConcurrentSet.createConcurrentSet<VideoSink>()

    override val contentHint = VideoContentHint.Motion

    private val DESIRED_CAPTURE_FORMAT = VideoCaptureFormat(960, 720, 30)
    private val ROTATION_360_DEGREES = 360

    private val TAG = "DefaultCameraCaptureSource"

    var eventAnalyticsController: EventAnalyticsController? = null
        set(value) {
            field = value
        }

    init {
        // Load library so that some of webrtc definition is linked properly
        System.loadLibrary("amazon_chime_media_client")
        val thread = HandlerThread("DefaultCameraCaptureSource")
        thread.start()
        handler = Handler(thread.looper)
    }

    override var device: MediaDevice? = MediaDevice.listVideoDevices(cameraManager)
        .firstOrNull { it.type == MediaDeviceType.VIDEO_FRONT_CAMERA } ?: MediaDevice.listVideoDevices(cameraManager)
        .firstOrNull { it.type == MediaDeviceType.VIDEO_BACK_CAMERA }
        set(value) {
            logger.info(TAG, "Setting capture device: $value")
            if (field == value) {
                logger.info(TAG, "Already using device: $value; ignoring")
                return
            }

            field = value

            // Restart capture if already running (i.e. we have a valid surface texture source)
            surfaceTextureSource?.let {
                stop()
                start()
            }
        }

    override fun switchCamera() {
        val desiredDeviceType = if (device?.type == MediaDeviceType.VIDEO_FRONT_CAMERA) {
            MediaDeviceType.VIDEO_BACK_CAMERA
        } else {
            MediaDeviceType.VIDEO_FRONT_CAMERA
        }
        device =
            MediaDevice.listVideoDevices(cameraManager).firstOrNull { it.type == desiredDeviceType } ?: MediaDevice.listVideoDevices(cameraManager)
                .firstOrNull { it.type == MediaDeviceType.VIDEO_BACK_CAMERA }

        if (device != null) {
            eventAnalyticsController?.pushHistory(MeetingHistoryEventName.videoInputSelected)
        }
    }

    override var torchEnabled: Boolean = false
        @RequiresApi(Build.VERSION_CODES.M)
        set(value) {
            if (cameraCharacteristics?.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == false) {
                logger.warn(
                    TAG,
                    "Torch not supported on current camera, setting value and returning"
                )
                return
            }

            field = value
            if (cameraDevice == null) {
                // If not in a session, use the CameraManager API
                device?.id?.let { cameraManager.setTorchMode(it, field) }
            } else {
                // Otherwise trigger a new request which will pick up the new value
                createCaptureRequest()
            }
        }

    override var format: VideoCaptureFormat = DESIRED_CAPTURE_FORMAT
        set(value) {
            logger.info(TAG, "Setting capture format: $value")
            if (field == value) {
                logger.info(TAG, "Already using format: $value; ignoring")
                return
            }

            field = VideoCaptureFormat(value.width, value.height, value.maxFps)

            // Restart capture if already running (i.e. we have a valid surface texture source)
            surfaceTextureSource?.let {
                stop()
                start()
            }
        }

    override fun start() {
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.CAMERA
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            handleCameraCaptureFail(CaptureSourceError.PermissionError)
            throw SecurityException("Missing necessary camera permissions")
        }
        stop()
        logger.info(TAG, "Camera capture start requested with device: $device")
        val device = device ?: run {
            logger.info(TAG, "Cannot start camera capture with null device")
            return
        }
        val id = device.id ?: run {
            logger.info(TAG, "Cannot start camera capture with null device id")
            return
        }

        cameraCharacteristics = cameraManager.getCameraCharacteristics(id).also {
            // Store these immediately for convenience
            sensorOrientation = it.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
            isCameraFrontFacing =
                it.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT
        }

        val chosenCaptureFormat: VideoCaptureFormat? =
            MediaDevice.listSupportedVideoCaptureFormats(cameraManager, device).minBy { format ->
                abs(format.width - this.format.width) + abs(format.height - this.format.height)
            }
        val surfaceTextureFormat: VideoCaptureFormat = chosenCaptureFormat ?: run {
            handleCameraCaptureFail(CaptureSourceError.ConfigurationFailure)
            return
        }
        surfaceTextureSource =
            surfaceTextureCaptureSourceFactory.createSurfaceTextureCaptureSource(
                surfaceTextureFormat.width,
                surfaceTextureFormat.height,
                contentHint
            )
        surfaceTextureSource?.addVideoSink(this)
        surfaceTextureSource?.start()

        cameraManager.openCamera(id, cameraDeviceStateCallback, handler)
    }

    override fun stop() {
        logger.info(TAG, "Stopping camera capture source")
        val sink: VideoSink = this
        runBlocking(handler.asCoroutineDispatcher().immediate) {
            // Close camera capture session
            cameraCaptureSession?.close()
            cameraCaptureSession = null

            // Close camera device, this will eventually trigger the stop callback
            cameraDevice?.close()
            cameraDevice = null

            // Stop surface capture source
            surfaceTextureSource?.removeVideoSink(sink)
            surfaceTextureSource?.stop()
            surfaceTextureSource?.release()
            surfaceTextureSource = null
        }
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        val processedBuffer: VideoFrameBuffer = createBufferWithUpdatedTransformMatrix(
            frame.buffer as VideoFrameTextureBuffer,
            isCameraFrontFacing, -sensorOrientation
        )

        val processedFrame =
            VideoFrame(frame.timestampNs, processedBuffer, getCaptureFrameRotation())
        sinks.forEach { it.onVideoFrameReceived(processedFrame) }
        processedBuffer.release()
    }

    override fun addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    override fun removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    override fun addCaptureSourceObserver(observer: CaptureSourceObserver) {
        observers.add(observer)
    }

    override fun removeCaptureSourceObserver(observer: CaptureSourceObserver) {
        observers.remove(observer)
    }

    fun release() {
        runBlocking(handler.asCoroutineDispatcher().immediate) {
            logger.info(TAG, "Stopping handler looper")
            handler.removeCallbacksAndMessages(null)
            handler.looper.quit()
        }
    }

    // Implement and store callbacks as private constants since we can't inherit from all of them
    // due to Kotlin not allowing multiple class inheritance

    private val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
        override fun onOpened(device: CameraDevice) {
            logger.info(TAG, "Camera device opened for ID ${device.id}")
            cameraDevice = device
            try {
                cameraDevice?.createCaptureSession(
                    listOf(surfaceTextureSource?.surface),
                    cameraCaptureSessionStateCallback,
                    handler
                )
            } catch (exception: CameraAccessException) {
                logger.info(
                    TAG,
                    "Exception encountered creating capture session: ${exception.reason}"
                )
                handleCameraCaptureFail(CaptureSourceError.SystemFailure)
                return
            }
        }

        override fun onClosed(device: CameraDevice) {
            logger.info(TAG, "Camera device closed for ID ${device.id}")
            ObserverUtils.notifyObserverOnMainThread(observers) { it.onCaptureStopped() }
        }

        override fun onDisconnected(device: CameraDevice) {
            logger.info(TAG, "Camera device disconnected for ID ${device.id}")
            ObserverUtils.notifyObserverOnMainThread(observers) { it.onCaptureStopped() }
        }

        override fun onError(device: CameraDevice, error: Int) {
            logger.info(TAG, "Camera device encountered error: $error for ID ${device.id}")
            handleCameraCaptureFail(CaptureSourceError.SystemFailure)
        }
    }

    private val cameraCaptureSessionStateCallback = object : CameraCaptureSession.StateCallback() {
        override fun onConfigured(session: CameraCaptureSession) {
            logger.info(
                TAG,
                "Camera capture session configured for session with device ID: ${session.device.id}"
            )
            cameraCaptureSession = session
            createCaptureRequest()
        }

        override fun onConfigureFailed(session: CameraCaptureSession) {
            logger.error(
                TAG, "Camera session configuration failed with device ID: ${session.device.id}"
            )
            handleCameraCaptureFail(CaptureSourceError.ConfigurationFailure)
            session.close()
        }
    }

    private fun handleCameraCaptureFail(error: CaptureSourceError) {
        val attributes = mutableMapOf<EventAttributeName, Any>(
            EventAttributeName.videoInputError to error
        )
        eventAnalyticsController?.publishEvent(EventName.videoInputFailed, attributes)
        ObserverUtils.notifyObserverOnMainThread(observers) {
            it.onCaptureFailed(error)
        }
    }

    private val cameraCaptureSessionCaptureCallback =
        object : CameraCaptureSession.CaptureCallback() {
            override fun onCaptureFailed(
                session: CameraCaptureSession,
                request: CaptureRequest,
                failure: CaptureFailure
            ) {
                logger.error(TAG, "Camera capture session failed: $failure")
                handleCameraCaptureFail(CaptureSourceError.SystemFailure)
            }
        }

    private fun createCaptureRequest() {
        val cameraDevice = cameraDevice ?: run {
            // This can occur occasionally if capture is restarted before the previous
            // completes. The next request will complete normally.
            logger.warn(TAG, "createCaptureRequest called without device set, may be mid restart")
            return
        }
        try {
            val captureRequestBuilder =
                cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)

            // Set target FPS
            val fpsRanges: Array<Range<Int>> =
                cameraCharacteristics?.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
                    ?: run {
                        logger.error(TAG, "Could not retrieve camera FPS ranges")
                        handleCameraCaptureFail(CaptureSourceError.ConfigurationFailure)
                        return
                    }
            // Pick range with max closest to but not exceeding the set max framerate
            val bestFpsRange = fpsRanges
                    .filter { it.upper <= this.format.maxFps }
                    .minBy { this.format.maxFps - it.upper }
                    ?: run {
                        logger.warn(TAG, "No FPS ranges below set max FPS")
                        // Just fall back to the closest
                        return@run fpsRanges.minBy { abs(this.format.maxFps - it.upper) }
                    } ?: run {
                        logger.error(TAG, "No valid FPS ranges")
                        handleCameraCaptureFail(CaptureSourceError.ConfigurationFailure)
                        return
                    }

            logger.info(TAG, "Setting target FPS range to $bestFpsRange")
            captureRequestBuilder.set(
                CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
                Range(bestFpsRange.lower, bestFpsRange.upper)
            )

            // Set target auto exposure mode
            captureRequestBuilder.set(
                CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON
            )
            captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false)

            // Set current torch status
            if (torchEnabled) {
                captureRequestBuilder.set(
                    CaptureRequest.FLASH_MODE,
                    CaptureRequest.FLASH_MODE_TORCH
                )
            } else {
                captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
            }

            setStabilizationMode(captureRequestBuilder)
            setFocusMode(captureRequestBuilder)

            captureRequestBuilder.addTarget(
                surfaceTextureSource?.surface
                    ?: throw UnknownError("Surface texture source should not be null")
            )
            cameraCaptureSession?.setRepeatingRequest(
                captureRequestBuilder.build(), cameraCaptureSessionCaptureCallback, handler
            )
            logger.info(
                TAG,
                "Capture request completed with device ID: ${cameraCaptureSession?.device?.id}"
            )
            ObserverUtils.notifyObserverOnMainThread(observers) {
                it.onCaptureStarted()
            }
        } catch (exception: CameraAccessException) {
            logger.error(
                TAG,
                "Failed to start capture request with device ID: ${cameraCaptureSession?.device?.id}, exception:$exception"
            )
            handleCameraCaptureFail(CaptureSourceError.SystemFailure)
            return
        }
    }

    private fun setStabilizationMode(captureRequestBuilder: CaptureRequest.Builder) {
        if (cameraCharacteristics?.get(
                CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION
            )?.any { it == CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON } == true
        ) {
            captureRequestBuilder[CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE] =
                CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON
            captureRequestBuilder[CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE] =
                CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF
            logger.info(TAG, "Using optical stabilization.")
            return
        }

        // If no optical mode is available, try software.
        if (cameraCharacteristics?.get(
                CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES
            )?.any { it == CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON } == true
        ) {
            captureRequestBuilder[CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE] =
                CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON
            captureRequestBuilder[CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE] =
                CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_OFF
            logger.info(TAG, "Using video stabilization.")
            return
        }

        logger.info(TAG, "Stabilization not available.")
    }

    private fun setFocusMode(captureRequestBuilder: CaptureRequest.Builder) {
        if (cameraCharacteristics?.get(
                CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES
            )?.any { it == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO } == true
        ) {
            captureRequestBuilder.set(
                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO
            )
            logger.info(TAG, "Using optical stabilization.")
            return
        }

        logger.info(TAG, "Auto-focus is not available.")
    }

    private fun getCaptureFrameRotation(): VideoRotation {
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        var rotation = when (windowManager.defaultDisplay.rotation) {
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            Surface.ROTATION_0 -> 0
            else -> 0
        }
        // Account for front cammera mirror
        if (!isCameraFrontFacing) {
            rotation = ROTATION_360_DEGREES - rotation
        }
        // Account for physical camera orientation
        rotation = (sensorOrientation + rotation) % ROTATION_360_DEGREES
        return VideoRotation.from(rotation) ?: VideoRotation.Rotation0
    }

    private fun createBufferWithUpdatedTransformMatrix(
        buffer: VideoFrameTextureBuffer,
        mirror: Boolean,
        rotation: Int
    ): VideoFrameTextureBuffer {
        val transformMatrix = Matrix()
        // Perform mirror and rotation around (0.5, 0.5) since that is the center of the texture.
        transformMatrix.preTranslate(0.5f, 0.5f)
        if (mirror) {
            // This negative scale mirrors across the vertical axis
            transformMatrix.preScale(-1f, 1f)
        }
        transformMatrix.preRotate(rotation.toFloat())
        transformMatrix.preTranslate(-0.5f, -0.5f)

        // The width and height are not affected by rotation
        val newMatrix = Matrix(buffer.transformMatrix)
        newMatrix.preConcat(transformMatrix)
        buffer.retain()
        return VideoFrameTextureBuffer(
            buffer.width,
            buffer.height,
            buffer.textureId,
            newMatrix,
            buffer.type,
            Runnable { buffer.release() })
    }
}


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

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

import android.content.Context
import android.content.Context.MEDIA_PROJECTION_SERVICE
import android.content.Intent
import android.content.res.Resources
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Handler
import android.os.HandlerThread
import android.util.DisplayMetrics
import android.view.Display
import android.view.Surface
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ConcurrentSet
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ObserverUtils
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import kotlin.math.ceil
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking

/**
 * [DefaultScreenCaptureSource] uses [MediaProjection] to create a [VirtualDisplay] to capture the
 * device screen. It will render the captured frames to a [Surface] provided by a [SurfaceTextureCaptureSourceFactory].
 *
 * Builders will need to get permission from users to obtain the [activityResultCode] and [activityData] arguments,
 * required to create an internal [MediaProjection] object.
 * Read [content share guide](https://github.com/aws/amazon-chime-sdk-android/blob/master/guides/content_share.md) for more information.
 *
 *  Note that you must finish starting the foreground service before calling start. Otherwise start
 *  will fail to succeed despite having the user grant permission and despite having created
 *  a foreground service for the [MediaProjection]. [CaptureSourceObserver.onCaptureFailed] will be
 *  called with [CaptureSourceError.SystemFailure] in this case.
 *
 *  If the [MediaProjection] could not be obtained with the [activityResultCode] and [activityData],
 *  [CaptureSourceObserver.onCaptureFailed] will be called with [CaptureSourceError.ConfigurationFailure].
 */
class DefaultScreenCaptureSource(
    private val context: Context,
    private val logger: Logger,
    private val surfaceTextureCaptureSourceFactory: SurfaceTextureCaptureSourceFactory,
    private val activityResultCode: Int,
    private val activityData: Intent,
    private val displayManager: DisplayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager,
    private val mediaProjectionManager: MediaProjectionManager = context.getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
) : VideoCaptureSource, VideoSink {
    private lateinit var displayMetrics: DisplayMetrics
    private var virtualDisplay: VirtualDisplay? = null

    // This source provides a surface we pass into the system APIs
    // and then starts emitting frames once the system starts drawing to the
    // surface. To speed up restart, since theses sources have to wait on
    // in-flight frames to finish release, we just begin the release and
    // create a new one
    private var surfaceTextureSource: SurfaceTextureCaptureSource? = null
    // See minFPS documentation in SurfaceTextureCaptureSource for motivation
    private val MIN_FPS = 15

    // Note that we intentionally choose to not reuse MediaProjection
    // between stop and start, as there are often (likely Android) bugs
    // resulting in cropped/glitched captures, or stalled capture
    // when one MediaProjection is used multiple times with different surfaces
    // or surfaces which have changed shape
    //
    // Since these are quick to create, we just take the result code and
    // data from the MediaProjection activity and create it within the source
    private var mediaProjection: MediaProjection? = null
    private val handler: Handler

    private val observers = mutableSetOf<CaptureSourceObserver>()

    // Concurrency modification could happen when sink gets
    // added/removed from another thread while sending frames
    private val sinks = ConcurrentSet.createConcurrentSet<VideoSink>()

    // This will prioritize resolution over framerate
    override val contentHint = VideoContentHint.Text

    // MediaProjection will only draw frames with the correct rotation
    // which means if the surface is recreated, it will, for example
    // draw a WxH frame on a HxW surface (it uses aspect fit).
    //
    // Therefore to avoid letterboxes we must track orientation and
    // restart when it changes to avoid the aforementioned issue.
    private var isOrientationInPortrait = true
    // This is used to block frames during restart, and to avoid
    // calling observers when we are just restarting
    private var isRestartingForOrientationChange = false

    private val TAG = "DefaultScreenCaptureSource"

    init {
        val thread = HandlerThread(TAG)
        thread.start()
        handler = Handler(thread.looper)
    }

    override fun start() {
        // This function is shared with logic which restarts following orientation changes, so post it
        // onto the handler for thread safety
        handler.post {
            val success = startInternal()

            // Set this to no-op any future restart requests on the handler
            isRestartingForOrientationChange = false

            if (success) {
                // Notify here so restarts do not trigger the callback
                ObserverUtils.notifyObserverOnMainThread(observers) {
                    it.onCaptureStarted()
                }
            }
        }
    }

    // Make an integer 16's multiples using bitwise and
    private fun alignNumberBy16(number: Int): Int {
        val maxIntAlignedBy16 = 0x7FFFFFF0
        return number and maxIntAlignedBy16
    }

    // Separate internal function since only some logic is shared between external calls
    // and internal restarts; must be called on handler
    private fun startInternal(): Boolean {
        if (mediaProjection != null) {
            logger.warn(TAG, "Screen capture has not been stopped before start request, stopping to release resources")
            stop()
        }
        logger.info(TAG, "Starting screen capture source")

        try {
            mediaProjection = mediaProjectionManager.getMediaProjection(activityResultCode, activityData)
        } catch (exception: SecurityException) {
            logger.error(TAG, "Failed to retrieve media projection due to SecurityException. The foreground service may not have finished starting before start was called.")

            ObserverUtils.notifyObserverOnMainThread(observers) {
                it.onCaptureFailed(CaptureSourceError.SystemFailure)
            }
            return false
        }

        if (mediaProjection == null) {
            logger.error(TAG, "Failed to retrieve media projection. The resultCode or data may have been invalid.")

            ObserverUtils.notifyObserverOnMainThread(observers) {
                it.onCaptureFailed(CaptureSourceError.ConfigurationFailure)
            }
            return false
        }

        // Note that these metrics depend on orientation
        displayMetrics = context.resources.displayMetrics
        val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
            ?: throw RuntimeException("No display found.")
        // Use `getRealMetrics` to properly account for menu, status bar, and rotation
        display.getRealMetrics(displayMetrics)

        val rotation = display.rotation
        isOrientationInPortrait = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180

        // Sometimes, Android changes displayMetrics widthPixels and heightPixels
        // and return inconsistent height and width for surfaceTextureSource VS virtualDisplay
        val width = displayMetrics.widthPixels
        val height = displayMetrics.heightPixels

        // Note that in landscape, for some reason `getRealMetrics` doesn't account for the status bar correctly
        // so we try to account for it with a manual adjustment to the surface size to avoid letterboxes
        // Some android device H.264 encoder is not able to handle size that is not multiples of 16 (such as Pixel3),
        // so screenCapture surface size is aligned manually to avoid the issue
        surfaceTextureSource =
            surfaceTextureCaptureSourceFactory.createSurfaceTextureCaptureSource(
                alignNumberBy16(width - (if (isOrientationInPortrait) 0 else getStatusBarHeight())),
                alignNumberBy16(height),
                contentHint
            )
        surfaceTextureSource?.minFps = MIN_FPS

        surfaceTextureSource?.addVideoSink(this)
        surfaceTextureSource?.start()

        virtualDisplay = mediaProjection?.createVirtualDisplay(
            TAG,
            width,
            height,
            displayMetrics.densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            surfaceTextureSource?.surface,
                object : VirtualDisplay.Callback() {
                    override fun onStopped() {
                        // Don't trigger observer for restart
                        if (!isRestartingForOrientationChange) {
                            ObserverUtils.notifyObserverOnMainThread(observers) {
                                it.onCaptureStopped()
                            }
                        }
                    }
                },
            handler
        )

        logger.info(TAG, "Media projection adapter activity succeeded, virtual display created")
        return true
    }

    override fun stop() {
        runBlocking(handler.asCoroutineDispatcher().immediate) {
            stopInternal()

            // Set this to no-op any future restart requests on the handler
            isRestartingForOrientationChange = false

            // Notify here so restarts do not trigger the callback
            ObserverUtils.notifyObserverOnMainThread(observers) {
                it.onCaptureStopped()
            }
        }
    }

    // Separate internal function since only some logic is shared between external calls
    // and internal restarts; must be called on handler
    private fun stopInternal() {
        logger.info(TAG, "Stopping screen capture source")

        mediaProjection?.stop()
        mediaProjection = null

        virtualDisplay?.release()
        virtualDisplay = null

        surfaceTextureSource?.removeVideoSink(this)
        surfaceTextureSource?.stop()
        surfaceTextureSource?.release()
        surfaceTextureSource = null
    }

    override fun addCaptureSourceObserver(observer: CaptureSourceObserver) {
        observers.add(observer)
    }

    override fun removeCaptureSourceObserver(observer: CaptureSourceObserver) {
        observers.remove(observer)
    }

    override fun addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    override fun removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        // Check if display rotation has changed; since this is
        // not an activity there is otherwise no way to observe this
        // besides checking every frame
        val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
            ?: throw RuntimeException("No display found.")
        val rotation = display.rotation
        val isOrientationInPortrait = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
        if (this.isOrientationInPortrait != isOrientationInPortrait) {
            isRestartingForOrientationChange = true
            logger.info(TAG, "Orientation changed from ${if (this.isOrientationInPortrait) "portrait" else "landscape"} " +
                "to ${if (isOrientationInPortrait) "portrait" else "landscape"}, restarting screen capture to update dimensions")
            // Post this task to avoid deadlock with the surface texture source handler
            handler.post {
                // Double check that start or stop hasn't been called since this was posted
                if (isRestartingForOrientationChange) {
                    stopInternal()
                    startInternal()
                    isRestartingForOrientationChange = false
                }
            }
            return
        }

        // Ignore frames while we are recreating the surface and display
        if (isRestartingForOrientationChange) return
        sinks.forEach { it.onVideoFrameReceived(frame) }
    }

    fun release() {
        runBlocking(handler.asCoroutineDispatcher().immediate) {
            logger.info(TAG, "Stopping handler looper")
            handler.removeCallbacksAndMessages(null)
            handler.looper.quit()
        }
    }

    // A combination of hardcoded methods to try to obtain or estimate the status bar height
    private fun getStatusBarHeight(): Int {
        val resources: Resources = context.resources
        val resourceId: Int = resources.getIdentifier("status_bar_height", "dimen", "android")
        return if (resourceId > 0) {
            resources.getDimensionPixelSize(resourceId)
        } else {
            ceil((if (VERSION.SDK_INT >= VERSION_CODES.M) 24 else 25) * resources.displayMetrics.density).toInt()
        }
    }
}


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

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

import android.annotation.SuppressLint
import android.graphics.SurfaceTexture
import android.opengl.EGL14
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.os.Handler
import android.os.HandlerThread
import android.view.Surface
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameTextureBuffer
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCore
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ConcurrentSet
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.video.TimestampAligner
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking

/**
 * [DefaultSurfaceTextureCaptureSource] will provide a [Surface] which it will listen to
 * and convert to [VideoFrameTextureBuffer] objects
 */
class DefaultSurfaceTextureCaptureSource(
    private val logger: Logger,
    private val width: Int,
    private val height: Int,
    override val contentHint: VideoContentHint = VideoContentHint.None,
    private val eglCoreFactory: EglCoreFactory
) : SurfaceTextureCaptureSource {
    // Publicly accessible
    override lateinit var surface: Surface

    // Additional graphics related state
    private var textureId: Int = 0
    private lateinit var surfaceTexture: SurfaceTexture
    private lateinit var eglCore: EglCore

    // EGLContext should be valid on this thread
    private val thread: HandlerThread = HandlerThread("DefaultSurfaceTextureCaptureSource")
    private val handler: Handler

    // This class helps align timestamps from the surface to timestamps
    // originating from different monotonic clocks in native code
    private val timestampAligner = TimestampAligner()

    // Frame available listener was called when a texture was already in use
    // SurfaceTextures must be updated by calling `updateTexImage` (usually in
    // OnFrameAvailableListener) however if something downstream is holding onto
    // a reference, then calling `updateTexImage` would invalidate their texture.
    // We use the release callback to trigger `updateTexImage` and the capture of
    // a new frame if that is the case.
    private var pendingAvailableFrame = false

    // Texture is in use, possibly in another thread. See comment above for why
    // we only allow one texture in flight at a time (i.e. because the SurfaceTexture owns
    // it, and can only own one texture at a time.
    private var textureBufferInFlight = false

    // Dispose has been called and we are waiting on texture to be released before we deallocate
    // non-GC-able resources
    private var releasePending = false

    // We will maintain close to the min FPS by simply posting a request to resend
    // a previously sent frame delayed by (1f/minFps)
    override var minFps = 0
    // Buffer the delay so it doesn't trigger on small framerate variations (will cause
    // min FPS to be slightly inaccurate but shouldn't have a huge impact given its purpose)
    private val RESEND_DELAY_BUFFER_MS = 10
    // Use this to check from the delayed request whether we have recently
    // enough sent a new frame
    private var lastAlignedTimestamp: Long? = null

    // Concurrency modification could happen when sink gets
    // added/removed from another thread while sending frames
    private var sinks = ConcurrentSet.createConcurrentSet<VideoSink>()

    private val TAG = "SurfaceTextureCaptureSource"

    private val DUMMY_PBUFFER_OFFSET = 0

    init {
        thread.start()
        handler = Handler(thread.looper)

        runBlocking(handler.asCoroutineDispatcher().immediate) {
            eglCore = eglCoreFactory.createEglCore()

            val surfaceAttributes =
                intArrayOf(EGL14.EGL_WIDTH, width, EGL14.EGL_HEIGHT, height, EGL14.EGL_NONE)
            eglCore.eglSurface = EGL14.eglCreatePbufferSurface(
                eglCore.eglDisplay,
                eglCore.eglConfig,
                surfaceAttributes,
                DUMMY_PBUFFER_OFFSET
            )
            EGL14.eglMakeCurrent(
                eglCore.eglDisplay,
                eglCore.eglSurface,
                eglCore.eglSurface,
                eglCore.eglContext
            )
            GlUtil.checkGlError("Failed to set dummy surface to initialize surface texture video source")

            textureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)

            surfaceTexture = SurfaceTexture(textureId)
            surfaceTexture.setDefaultBufferSize(width, height)
            @SuppressLint("Recycle")
            surface = Surface(surfaceTexture)

            logger.info(
                TAG,
                "Created surface texture for video source with dimensions $width x $height"
            )
        }
    }

    override fun start() {
        handler.post {
            surfaceTexture.setOnFrameAvailableListener({
                pendingAvailableFrame = true
                tryCapturingFrame()
            }, handler)
        }
    }

    override fun stop() {
        runBlocking(handler.asCoroutineDispatcher().immediate) {
            logger.info(TAG, "Setting on frame available listener to null")
            surfaceTexture.setOnFrameAvailableListener(null)
        }
    }

    // Unused
    override fun addCaptureSourceObserver(observer: CaptureSourceObserver) {}
    override fun removeCaptureSourceObserver(observer: CaptureSourceObserver) {}

    override fun addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    override fun removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    override fun release() {
        handler.post {
            logger.info(TAG, "Releasing surface texture capture source")
            if (!textureBufferInFlight) {
                completeRelease()
            } else {
                // If we have a frame in flight we cannot immediately release
                // This will cause onFrameRelease to trigger completeRelease
                releasePending = true
            }
        }
    }

    private fun tryCapturingFrame() {
        // Check to see if we are in valid state for capturing a new frame
        //  * Don't capture a new frame if we are in the process of releasing
        //  * Don't capture a new frame if we don't have one pending
        //  * Don't capture a new frame if there is a buffer in flight as to not invalidate it
        if (releasePending || !pendingAvailableFrame || textureBufferInFlight) {
            return
        }

        textureBufferInFlight = true
        pendingAvailableFrame = false

        // This call is what actually updates the texture
        surfaceTexture.updateTexImage()

        val transformMatrix = FloatArray(16)
        surfaceTexture.getTransformMatrix(transformMatrix)

        val buffer =
            VideoFrameTextureBuffer(
                width,
                height,
                textureId,
                GlUtil.convertToMatrix(transformMatrix),
                VideoFrameTextureBuffer.Type.TEXTURE_OES,
                Runnable { onFrameReleased() })
        val alignedTimestamp = timestampAligner.translateTimestamp(surfaceTexture.timestamp)
        val frame = VideoFrame(alignedTimestamp, buffer)

        sinks.forEach { it.onVideoFrameReceived(frame) }
        frame.release()

        // If we have min FPS set, check in a while if we should resend the previous frame
        if (minFps > 0) {
            lastAlignedTimestamp = alignedTimestamp
            val resendDelayMs = RESEND_DELAY_BUFFER_MS + (1f / minFps) * 1000
            handler.postDelayed({
                if (alignedTimestamp == lastAlignedTimestamp) {
                    pendingAvailableFrame = true
                    tryCapturingFrame()
                }
            }, resendDelayMs.toLong())
        }
    }

    private fun onFrameReleased() {
        // Cannot assume this occurs on correct thread
        handler.post {
            textureBufferInFlight = false
            if (releasePending) {
                completeRelease()
            } else {
                // May have pending frame which was waiting on the previous
                // frame to no longer have users so that that texture can be
                // reused
                tryCapturingFrame()
            }
        }
    }

    private fun completeRelease() {
        GLES20.glDeleteTextures(1, intArrayOf(textureId), 0)
        surfaceTexture.release()
        surface.release()

        eglCore.release()

        timestampAligner.dispose()
        logger.info(TAG, "Finished releasing surface texture capture source")

        handler.looper.quit()
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

/**
 * [DefaultSurfaceTextureCaptureSourceFactory] creates [DefaultSurfaceTextureCaptureSource] objects
 */
class DefaultSurfaceTextureCaptureSourceFactory(
    private val logger: Logger,
    private val eglCoreFactory: EglCoreFactory
) : SurfaceTextureCaptureSourceFactory {
    override fun createSurfaceTextureCaptureSource(
        width: Int,
        height: Int,
        contentHint: VideoContentHint
    ): SurfaceTextureCaptureSource {
        check(width >= 0 && height >= 0) { "Width and height must be positive" }
        return DefaultSurfaceTextureCaptureSource(
            logger,
            width,
            height,
            contentHint,
            eglCoreFactory
        )
    }
}


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

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

import android.view.Surface
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource

/**
 * [SurfaceTextureCaptureSource] provides a [Surface] which can be passed to system sources like the camera.
 * Upon [start] call, the source will listen to the surface and emit any new images as [VideoFrame] objects to any
 * downstream [VideoSink] interfaces. This class is mostly intended for composition within [VideoSource] implementations which will
 * pass the created [Surface] to a system source, then call [addVideoSink] to receive the frames before transforming and
 * passing downstream.
 */
interface SurfaceTextureCaptureSource : VideoCaptureSource {
    /**
     * [Surface] from which any buffers submitted to will be emitted as a [VideoFrame].
     * User must call [start] to start listening, and [stop] will likewise stop listening.
     */
    val surface: Surface

    /**
     * Setting this to a positive value will lead the source to resend previously captured frames
     * as necessary to approximately maintain the set value.
     *
     * This is useful for applications like screen capture where the system does not capture when
     * there is no motion on screen. Continuing to send frames helps the encoder improve the quality
     * on the receiving end, and also helps mitigate possible issues with downstream frame throttling
     * dropping frames before the end of an animation.
     */
    var minFps: Int

    /**
     * Deallocate any state or resources held by this object. Not possible to reuse after call. Surface
     * will have been released.
     */
    fun release()
}


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

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

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

/**
 * [SurfaceTextureCaptureSourceFactory] is an factory interface for creating new [SurfaceTextureCaptureSource] objects,
 * possible using shared state. This provides flexibility over use of [SurfaceTextureCaptureSource] objects since
 * they may not allow reuse, or may have a delay before possible reuse.
 */
interface SurfaceTextureCaptureSourceFactory {
    /**
     * Create a new [SurfaceTextureCaptureSource] object
     *
     * @return [SurfaceTextureCaptureSource] - Newly created and initialized [SurfaceTextureCaptureSource] object
     */
    fun createSurfaceTextureCaptureSource(
        width: Int,
        height: Int,
        contentHint: VideoContentHint
    ): SurfaceTextureCaptureSource
}


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

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

/**
 * [VideoCaptureFormat] describes a given capture format that can be set to a [VideoCaptureSource].
 * Note that [VideoCaptureSource] implementations may ignore or adjust unsupported values.
 */
data class VideoCaptureFormat(
    /**
     * Capture width
     */
    val width: Int,

    /**
     * Capture height
     */
    val height: Int,

    /**
     * Max FPS. When used as input this implies the desired FPS as well
     */
    val maxFps: Int
) {
    init {
        check(width >= 0 && height >= 0) { "Width and height must be positive" }
    }

    override fun toString(): String {
        return "$width x $height @ $maxFps FPS"
    }
}


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

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

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

/**
 * [VideoCaptureSource] is an interface for various video capture sources (i.e. screen, camera, file) which can emit [VideoFrame] objects
 * All the APIs here can be called regardless of whether the [AudioVideoFacade] is started or not.
 */
interface VideoCaptureSource :
    VideoSource {
    /**
     * Start capturing on this source and emitting video frames
     */
    fun start()

    /**
     * Stop capturing on this source and cease emitting video frames
     */
    fun stop()

    /**
     * Add a capture source observer to receive callbacks from the source on lifecycle events
     * which can be used to trigger UI. This observer is entirely optional.
     *
     * @param observer: [CaptureSourceObserver] - New observer
     */
    fun addCaptureSourceObserver(observer: CaptureSourceObserver)

    /**
     * Remove a capture source observer
     *
     * @param observer: [CaptureSourceObserver] - Observer to remove
     */
    fun removeCaptureSourceObserver(observer: CaptureSourceObserver)
}