From fa90a6458252e3bcaaf8aa4f923a73c16e3e5d03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com>
Date: Fri, 26 Nov 2021 11:36:35 -0500
Subject: [PATCH] video: manage camera per id

Change-Id: I55b7ad40de20f613e95e76823faf5b5904b96226
---
 .../java/cx/ring/services/CameraService.kt    | 335 +++++++++++-------
 .../cx/ring/services/HardwareServiceImpl.kt   | 114 ++----
 .../kotlin/net/jami/call/CallPresenter.kt     |   2 +-
 .../kotlin/net/jami/services/DaemonService.kt |   6 +-
 .../net/jami/services/HardwareService.kt      |  12 +-
 5 files changed, 251 insertions(+), 218 deletions(-)

diff --git a/ring-android/app/src/main/java/cx/ring/services/CameraService.kt b/ring-android/app/src/main/java/cx/ring/services/CameraService.kt
index 6c5f09d6f..0a930464c 100644
--- a/ring-android/app/src/main/java/cx/ring/services/CameraService.kt
+++ b/ring-android/app/src/main/java/cx/ring/services/CameraService.kt
@@ -60,30 +60,38 @@ import kotlin.math.max
 
 class CameraService internal constructor(c: Context) {
     private val manager = c.getSystemService(Context.CAMERA_SERVICE) as CameraManager?
-    private val mParams = HashMap<String?, VideoParams>()
+    private val mParams = HashMap<String, VideoParams>()
     private val mNativeParams: MutableMap<String, DeviceParams> = HashMap()
     private val t = HandlerThread("videoHandler")
     private val videoLooper: Looper
         get() = t.apply { if (state == Thread.State.NEW) start() }.looper
-    val videoHandler: Handler by lazy { Handler(videoLooper) }
-    private var previewCamera: CameraDevice? = null
-    private var currentMediaProjection: MediaProjection? = null
-    private var virtualDisplay: VirtualDisplay? = null
-    private var currentCodec: MediaCodec? = null
+    private val videoHandler: Handler by lazy { Handler(videoLooper) }
 
     // SPS and PPS NALs (Config Data).
     private var codecData: ByteBuffer? = null
     private val maxResolutionSubject: Subject<Pair<Int?, Int?>> = BehaviorSubject.createDefault(RESOLUTION_NONE)
-    protected var devices: VideoDevices? = null
-    private var previewParams: VideoParams? = null
+    private var devices: VideoDevices? = null
     private val availabilityCallback: AvailabilityCallback = object : AvailabilityCallback() {
         override fun onCameraAvailable(cameraId: String) {
             Log.w(TAG, "onCameraAvailable $cameraId")
-            filterCompatibleCamera(arrayOf(cameraId), manager!!).forEach { camera: Pair<String, CameraCharacteristics> ->
+            filterCompatibleCamera(
+                arrayOf(cameraId),
+                manager!!
+            ).forEach { camera: Pair<String, CameraCharacteristics> ->
+                val devices = devices ?: return
                 synchronized(addedDevices) {
-                    if (addedDevices.add(camera.first)) {
-                        if (!devices!!.cameras.contains(camera.first)) devices!!.cameras.add(camera.first)
-                        JamiService.addVideoDevice(camera.first)
+                    if (!devices.cameras.contains(camera.first)) {
+                        when (camera.second.get(CameraCharacteristics.LENS_FACING)) {
+                            CameraCharacteristics.LENS_FACING_FRONT -> if (devices.cameraFront == null) {
+                                devices.addCamera(camera.first)
+                                devices.cameraFront = camera.first
+                            }
+                            CameraCharacteristics.LENS_FACING_BACK -> if (devices.cameraBack == null) {
+                                devices.addCamera(camera.first)
+                                devices.cameraBack = camera.first
+                            }
+                            else -> devices.addCamera(camera.first)
+                        }
                     }
                 }
             }
@@ -92,9 +100,14 @@ class CameraService internal constructor(c: Context) {
         override fun onCameraUnavailable(cameraId: String) {
             if (devices == null || devices!!.currentId == null || devices!!.currentId != cameraId) {
                 synchronized(addedDevices) {
+                    val devices = devices ?: return
+                    devices.cameras.remove(cameraId)
+                    if (devices.cameraFront == cameraId)
+                        devices.cameraFront = null
+                    if (devices.cameraBack == cameraId)
+                        devices.cameraBack = null
                     if (addedDevices.remove(cameraId)) {
-                        Log.w(TAG, "onCameraUnavailable $cameraId current:$previewCamera")
-                        devices!!.cameras.remove(cameraId)
+                        Log.w(TAG, "onCameraUnavailable $cameraId")
                         JamiService.removeVideoDevice(cameraId)
                     }
                 }
@@ -109,6 +122,7 @@ class CameraService internal constructor(c: Context) {
         var currentId: String? = null
         var currentIndex = 0
         var cameraFront: String? = null
+        var cameraBack: String? = null
         fun switchInput(setDefaultCamera: Boolean): String? {
             if (setDefaultCamera && cameras.isNotEmpty()) {
                 currentId = cameras[0]
@@ -121,6 +135,13 @@ class CameraService internal constructor(c: Context) {
             return currentId
         }
 
+        fun addCamera(cameraId: String) {
+            cameras.add(cameraId)
+            if (addedDevices.add(cameraId)) {
+                JamiService.addVideoDevice(cameraId)
+            }
+        }
+
         companion object {
             const val SCREEN_SHARING = "desktop"
         }
@@ -134,10 +155,7 @@ class CameraService internal constructor(c: Context) {
         Log.w(TAG, "getParams() $camId")
         if (camId != null) {
             return mParams[camId]
-        } else if (previewParams != null) {
-            Log.w(TAG, "getParams() previewParams")
-            return previewParams
-        } else if (devices != null && devices!!.cameras.isNotEmpty()) {
+        } else if (!devices?.cameras.isNullOrEmpty()) {
             Log.w(TAG, "getParams() fallback")
             devices!!.currentId = devices!!.cameras[0]
             return mParams[devices!!.currentId]
@@ -145,10 +163,6 @@ class CameraService internal constructor(c: Context) {
         return null
     }
 
-    fun setPreviewParams(params: VideoParams?) {
-        previewParams = params
-    }
-
     fun setParameters(camId: String, format: Int, width: Int, height: Int, rate: Int, rotation: Int) {
         Log.w(TAG, "setParameters() $camId $format $width $height $rate $rotation")
         val deviceParams = mNativeParams[camId]
@@ -161,8 +175,6 @@ class CameraService internal constructor(c: Context) {
             params = VideoParams(camId, deviceParams.size.x, deviceParams.size.y, rate)
             mParams[camId] = params
         } else {
-            //params.id = camId;
-            //params.format = format;
             params.width = deviceParams.size.x
             params.height = deviceParams.size.y
             params.rate = rate
@@ -188,14 +200,20 @@ class CameraService internal constructor(c: Context) {
         JamiService.setDeviceOrientation(camId, rotation)
     }
 
-    fun getCameraInfo(camId: String, formats: IntVect?, sizes: UintVect, rates: UintVect, minVideoSize: Point, context: Context) {
-        Log.d(TAG, "getCameraInfo: $camId min. size: $minVideoSize")
+    fun getCameraInfo(
+        camId: String,
+        formats: IntVect?,
+        sizes: UintVect,
+        rates: UintVect,
+        minVideoSize: Point,
+        context: Context
+    ) {
+        Log.d(TAG, "getCameraInfo: $camId min size: $minVideoSize")
         val p = DeviceParams()
         rates.clear()
         fillCameraInfo(p, camId, formats, sizes, rates, minVideoSize, context)
         sizes.add(p.size.x.toLong())
         sizes.add(p.size.y.toLong())
-        Log.d(TAG, "getCameraInfo: " + camId + " max. size: " + p.maxSize + " size:" + p.size)
         mNativeParams[camId] = p
     }
 
@@ -223,13 +241,20 @@ class CameraService internal constructor(c: Context) {
         return cameraCount > 0
     }
 
-    class VideoParams(val id: String, var width: Int, var height: Int, var rate: Int) {
+    data class VideoParams(val id: String, var width: Int, var height: Int, var rate: Int) {
         val inputUri: String = "camera://$id"
 
         //public int format;
         // size as captured by Android
         var rotation = 0
         var codec: String? = null
+        var isCapturing: Boolean = false
+        var camera: CameraDevice? = null
+        var projection: MediaProjection? = null
+        var display: VirtualDisplay? = null
+        var mediaCodec: MediaCodec? = null
+        var codecStarted: Boolean = false
+
         fun getAndroidCodec(): String {
             return when (val codec = codec) {
                 "H264" -> MediaFormat.MIMETYPE_VIDEO_AVC
@@ -260,17 +285,20 @@ class CameraService internal constructor(c: Context) {
     private fun loadDevices(manager: CameraManager): Single<VideoDevices> {
         return Single.fromCallable {
             val devices = VideoDevices()
-            val cameras = filterCompatibleCamera(manager.cameraIdList, manager).toList()
-            val backCamera = filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_BACK).first()
-            val frontCamera = filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_FRONT).first()
+            val cameras = filterCompatibleCamera(manager.cameraIdList, manager)
+            val backCamera = filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_BACK).firstOrNull()
+            val frontCamera = filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_FRONT).firstOrNull()
             val externalCameras: List<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_EXTERNAL)
             } else {
                 emptyList()
             }
-            (listOf(frontCamera, backCamera) + externalCameras).forEach { id -> devices.cameras.add(id) }
+            if (frontCamera != null) devices.cameras.add(frontCamera)
+            if (backCamera != null) devices.cameras.add(backCamera)
+            devices.cameras.addAll(externalCameras)
             if (devices.cameras.isNotEmpty()) devices.currentId = devices.cameras[0]
             devices.cameraFront = frontCamera
+            devices.cameraBack = backCamera
             Log.w(TAG, "Loading video devices: found " + devices.cameras.size)
             devices
         }.subscribeOn(AndroidSchedulers.from(videoLooper))
@@ -309,7 +337,7 @@ class CameraService internal constructor(c: Context) {
                 devs
             }
             .ignoreElement()
-            .doOnError { e: Throwable? ->
+            .doOnError { e: Throwable ->
                 Log.e(TAG, "Error initializing video device", e)
                 maxResolutionSubject.onNext(RESOLUTION_NONE)
             }
@@ -327,13 +355,18 @@ class CameraService internal constructor(c: Context) {
         fun onError()
     }
 
-    fun closeCamera() {
-        previewCamera?.let { camera ->
-            previewCamera = null
-            camera.close()
-            currentCodec = null
+    fun closeCamera(camId: String) {
+        mParams[camId]?.let { params ->
+            params.camera?.let { camera ->
+                camera.close()
+                params.camera = null
+            }
+            params.projection?.let { mediaProjection ->
+                mediaProjection.stop()
+                params.projection = null
+            }
+            params.isCapturing = false
         }
-        stopScreenSharing()
     }
 
     private fun openEncoder(
@@ -341,11 +374,12 @@ class CameraService internal constructor(c: Context) {
         mimeType: String?,
         handler: Handler,
         resolution: Int,
-        bitrate: Int
+        bitrate: Int,
+        surface: Surface? = null
     ): Pair<MediaCodec?, Surface?> {
         Log.d(TAG, "Video with codec " + mimeType + " resolution: " + videoParams.width + "x" + videoParams.height + " Bitrate: " + bitrate)
         val bitrateValue: Int = if (bitrate == 0) if (resolution >= 720) 192 * 8 * 1024 else 100 * 8 * 1024 else bitrate * 8 * 1024
-        val frameRate = 30 // 30 fps
+        val frameRate = videoParams.rate//30 // 30 fps
         val format = MediaFormat.createVideoFormat(mimeType!!, videoParams.width, videoParams.height).apply {
             setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0)
             setInteger(MediaFormat.KEY_BIT_RATE, bitrateValue)
@@ -375,7 +409,7 @@ class CameraService internal constructor(c: Context) {
             params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrateValue)
             codec.setParameters(params)
             codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
-            encoderInput = codec.createInputSurface()
+            encoderInput = /*surface?.also { codec!!.setInputSurface(it) } ?:*/ codec.createInputSurface()
             val callback: MediaCodec.Callback = object : MediaCodec.Callback() {
                 override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
                 override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
@@ -383,8 +417,9 @@ class CameraService internal constructor(c: Context) {
                         if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM == 0) {
                             // Get and cache the codec data (SPS/PPS NALs)
                             val isConfigFrame = info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0
+                            val buffer = codec.getOutputBuffer(index)
                             if (isConfigFrame) {
-                                codec.getOutputBuffer(index)?.let { outputBuffer ->
+                                buffer?.let { outputBuffer ->
                                     outputBuffer.position(info.offset)
                                     outputBuffer.limit(info.offset + info.size)
                                     codecData = ByteBuffer.allocateDirect(info.size).apply {
@@ -393,22 +428,36 @@ class CameraService internal constructor(c: Context) {
                                     }
                                     Log.i(TAG, "Cache new codec data (SPS/PPS, ...)")
                                 }
-                                codec.releaseOutputBuffer(index, false)
                             } else {
                                 val isKeyFrame = info.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME != 0
                                 // If it's a key-frame, send the cached SPS/PPS NALs prior to
                                 // sending key-frame.
                                 if (isKeyFrame) {
                                     codecData?.let { data ->
-                                        JamiService.captureVideoPacket(videoParams.inputUri, data, data.capacity(), 0, false, info.presentationTimeUs, videoParams.rotation)
+                                        JamiService.captureVideoPacket(
+                                            videoParams.inputUri,
+                                            data,
+                                            data.capacity(),
+                                            0,
+                                            false,
+                                            info.presentationTimeUs,
+                                            videoParams.rotation
+                                        )
                                     }
                                 }
 
                                 // Send the encoded frame
-                                val buffer = codec.getOutputBuffer(index)
-                                JamiService.captureVideoPacket(videoParams.inputUri, buffer, info.size, info.offset, isKeyFrame, info.presentationTimeUs,videoParams.rotation)
-                                codec.releaseOutputBuffer(index, false)
+                                JamiService.captureVideoPacket(
+                                    videoParams.inputUri,
+                                    buffer,
+                                    info.size,
+                                    info.offset,
+                                    isKeyFrame,
+                                    info.presentationTimeUs,
+                                    videoParams.rotation
+                                )
                             }
+                            codec.releaseOutputBuffer(index, false)
                         }
                     } catch (e: IllegalStateException) {
                         Log.e(TAG, "MediaCodec can't process buffer", e)
@@ -445,45 +494,52 @@ class CameraService internal constructor(c: Context) {
     fun requestKeyFrame() {
         Log.w(TAG, "requestKeyFrame()")
         try {
-            if (currentCodec != null) {
+            /*if (currentCodec != null) {
                 val params = Bundle()
                 params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
                 currentCodec!!.setParameters(params)
-            }
+            }*/
         } catch (e: IllegalStateException) {
             Log.w(TAG, "Can't send keyframe request", e)
         }
     }
 
-    fun setBitrate(bitrate: Int) {
-        Log.w(TAG, "setBitrate() $bitrate")
+    fun setBitrate(camId: String, bitrate: Int) {
+        Log.w(TAG, "setBitrate() $camId $bitrate")
         try {
-            if (currentCodec != null) {
-                val params = Bundle()
-                params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate * 1024)
-                currentCodec!!.setParameters(params)
-            }
+            mParams[camId]?.mediaCodec?.setParameters(Bundle().apply {
+                putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate * 1024)
+            })
         } catch (e: IllegalStateException) {
             Log.w(TAG, "Can't set bitrate", e)
         }
     }
 
-    private fun createVirtualDisplay(projection: MediaProjection, metrics: DisplayMetrics): Pair<MediaCodec?, VirtualDisplay>? {
-        var screenWidth = metrics.widthPixels
-        var screenHeight = metrics.heightPixels
+    private fun createVirtualDisplay(
+        params: VideoParams,
+        projection: MediaProjection,
+        surface: TextureView,
+        metrics: DisplayMetrics
+    ): Pair<MediaCodec?, VirtualDisplay>? {
         val screenDensity = metrics.densityDpi
         val handler = videoHandler
         var r: Pair<MediaCodec?, Surface?>? = null
-        while (max(screenWidth, screenHeight) > 1920) {
-            screenWidth /= 2
-            screenHeight /= 2
-        }
-        while (screenWidth >= 320) {
-            val params = VideoParams(VideoDevices.SCREEN_SHARING, screenWidth, screenHeight, 24)
-            r = openEncoder(params, MediaFormat.MIMETYPE_VIDEO_AVC, handler, 720, 0)
+        if (params.rate == 0) {
+            params.rate = 24
+        }
+        while (params.width >= 320) {
+            Log.e(TAG, "createVirtualDisplay ${params.width}x${params.height} at ${params.rate}")
+            r = openEncoder(
+                params,
+                MediaFormat.MIMETYPE_VIDEO_AVC,
+                handler,
+                720,
+                0,
+                surface = Surface(surface.surfaceTexture)
+            )
             if (r.first == null) {
-                screenWidth /= 2
-                screenHeight /= 2
+                params.width /= 2
+                params.height /= 2
             } else break
         }
         if (r == null) return null
@@ -491,27 +547,30 @@ class CameraService internal constructor(c: Context) {
         val codec = r.first
         codec?.start()
         return try {
-            Pair(codec, projection.createVirtualDisplay("ScreenSharing", screenWidth, screenHeight, screenDensity,
-                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, object : VirtualDisplay.Callback() {
-                    override fun onPaused() {
-                        Log.w(TAG, "VirtualDisplay.onPaused")
-                    }
+            Pair(
+                codec, projection.createVirtualDisplay(
+                    "ScreenSharing", params.width, params.height, screenDensity,
+                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, object : VirtualDisplay.Callback() {
+                        override fun onPaused() {
+                            Log.w(TAG, "VirtualDisplay.onPaused")
+                        }
 
-                    override fun onResumed() {
-                        Log.w(TAG, "VirtualDisplay.onResumed")
-                    }
+                        override fun onResumed() {
+                            Log.w(TAG, "VirtualDisplay.onResumed")
+                        }
 
-                    override fun onStopped() {
-                        Log.w(TAG, "VirtualDisplay.onStopped")
-                        if (surface != null) {
-                            surface.release()
-                            codec?.release()
-                            if (currentCodec == codec) currentCodec = null
+                        override fun onStopped() {
+                            Log.w(TAG, "VirtualDisplay.onStopped")
+                            if (surface != null) {
+                                surface.release()
+                                codec?.release()
+                            }
                         }
-                    }
-                }, handler
-            ))
+                    }, handler
+                )
+            )
         } catch (e: Exception) {
+            Log.e(TAG, "Exception creating virtual display", e)
             if (codec != null) {
                 codec.stop()
                 codec.release()
@@ -521,28 +580,37 @@ class CameraService internal constructor(c: Context) {
         }
     }
 
-    fun startScreenSharing(mediaProjection: MediaProjection, metrics: DisplayMetrics): Boolean {
-        val r = createVirtualDisplay(mediaProjection, metrics)
+    fun startScreenSharing(
+        params: VideoParams,
+        mediaProjection: MediaProjection,
+        surface: TextureView,
+        metrics: DisplayMetrics
+    ): Boolean {
+        val r = createVirtualDisplay(params, mediaProjection, surface, metrics)
         if (r != null) {
-            currentMediaProjection = mediaProjection
-            currentCodec = r.first
-            virtualDisplay = r.second
+            mediaProjection.registerCallback(object : MediaProjection.Callback() {
+                override fun onStop() {
+                    params.mediaCodec?.let { codec ->
+                        codec.signalEndOfInputStream()
+                        codec.stop()
+                        codec.release()
+                        params.mediaCodec = null
+                    }
+                    params.display?.release()
+                    params.display = null
+                    params.codecStarted = false
+                }
+            }, videoHandler)
+            params.codecStarted = true
+            params.mediaCodec = r.first
+            params.projection = mediaProjection
+            params.display = r.second
             return true
         }
+        mediaProjection.stop()
         return false
     }
 
-    fun stopScreenSharing() {
-        if (virtualDisplay != null) {
-            virtualDisplay!!.release()
-            virtualDisplay = null
-        }
-        if (currentMediaProjection != null) {
-            currentMediaProjection!!.stop()
-            currentMediaProjection = null
-        }
-    }
-
     fun openCamera(
         videoParams: VideoParams,
         surface: TextureView,
@@ -551,7 +619,7 @@ class CameraService internal constructor(c: Context) {
         resolution: Int,
         bitrate: Int
     ) {
-        previewCamera?.close()
+        //previewCamera?.close()
         val handler = videoHandler
         try {
             val view = surface as AutoFitTextureView
@@ -589,12 +657,13 @@ class CameraService internal constructor(c: Context) {
                 targets.add(tmpReader.surface)
             }
             val reader = tmpReader
-            val codecStarted = booleanArrayOf(false)
             manager.openCamera(videoParams.id, object : CameraDevice.StateCallback() {
                 override fun onOpened(camera: CameraDevice) {
                     try {
                         Log.w(TAG, "onOpened " + videoParams.id)
-                        previewCamera = camera
+                        //previewCamera = camera
+                        videoParams.camera = camera
+                        videoParams.mediaCodec = codec?.first
                         texture!!.setDefaultBufferSize(previewSize.width, previewSize.height)
                         val builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
                         builder.addTarget(s)
@@ -615,11 +684,16 @@ class CameraService internal constructor(c: Context) {
                                 try {
                                     session.setRepeatingRequest(request,
                                         if (codec?.second != null) object : CaptureCallback() {
-                                            override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
+                                            override fun onCaptureStarted(
+                                                session: CameraCaptureSession,
+                                                request: CaptureRequest,
+                                                timestamp: Long,
+                                                frameNumber: Long
+                                            ) {
                                                 if (frameNumber == 1L) {
                                                     try {
-                                                        codec.first!!.start()
-                                                        codecStarted[0] = true
+                                                        videoParams.mediaCodec?.start()
+                                                        videoParams.codecStarted = true
                                                     } catch (e: Exception) {
                                                         listener.onError()
                                                     }
@@ -627,9 +701,6 @@ class CameraService internal constructor(c: Context) {
                                             }
                                         } else null,
                                         handler)
-                                    if (codec?.first != null) {
-                                        currentCodec = codec.first
-                                    }
                                 } catch (e: Exception) {
                                     Log.w(TAG, "onConfigured error:", e)
                                     camera.close()
@@ -665,16 +736,14 @@ class CameraService internal constructor(c: Context) {
                 override fun onClosed(camera: CameraDevice) {
                     Log.w(TAG, "onClosed")
                     try {
-                        if (previewCamera === camera) previewCamera = null
-                        if (codec != null) {
-                            if (codec.first != null) {
-                                if (codecStarted[0]) codec.first!!.signalEndOfInputStream()
-                                codec.first!!.release()
-                                if (codec.first == currentCodec) currentCodec = null
-                                codecStarted[0] = false
-                            }
-                            if (codec.second != null) codec.second!!.release()
+                        videoParams.mediaCodec?.let { mediaCodec ->
+                            if (videoParams.codecStarted)
+                                mediaCodec.signalEndOfInputStream()
+                            mediaCodec.release()
+                            videoParams.mediaCodec = null
+                            videoParams.codecStarted = false
                         }
+                        codec?.second?.release()
                         reader?.close()
                         s.release()
                     } catch (e: Exception) {
@@ -689,8 +758,6 @@ class CameraService internal constructor(c: Context) {
         }
     }
 
-    val isOpen: Boolean
-        get() = previewCamera != null || currentMediaProjection != null
     val cameraIds: List<String>
         get() = devices?.cameras ?: ArrayList()
     val cameraCount: Int
@@ -715,12 +782,14 @@ class CameraService internal constructor(c: Context) {
                 val metrics = context.resources.displayMetrics
                 var screenWidth = metrics.widthPixels
                 var screenHeight = metrics.heightPixels
-                while (max(screenWidth, screenHeight) > 1920) {
+                while (max(screenWidth, screenHeight) > max(minVideoSize.x, minVideoSize.y)) {
                     screenWidth /= 2
                     screenHeight /= 2
                 }
                 p.size.x = screenWidth
                 p.size.y = screenHeight
+                p.rate = 24
+                rates.add(p.rate)
                 return
             }
             val cc = manager.getCameraCharacteristics(camId)
@@ -792,7 +861,10 @@ class CameraService internal constructor(c: Context) {
             return (180 - rotation + 180) % 360
         }
 
-        private fun filterCompatibleCamera(cameras: Array<String>, cameraManager: CameraManager): List<Pair<String, CameraCharacteristics>> {
+        private fun filterCompatibleCamera(
+            cameras: Array<String>,
+            cameraManager: CameraManager
+        ): List<Pair<String, CameraCharacteristics>> {
             return cameras.map { id: String -> Pair(id, cameraManager.getCameraCharacteristics(id)) }
                 .filter { camera: Pair<String, CameraCharacteristics> ->
                     try {
@@ -806,12 +878,14 @@ class CameraService internal constructor(c: Context) {
                 }
         }
 
-        private fun filterCameraIdsFacing(cameras: List<Pair<String, CameraCharacteristics>>, facing: Int): List<String> {
+        private fun filterCameraIdsFacing(
+            cameras: List<Pair<String, CameraCharacteristics>>,
+            facing: Int
+        ): List<String> {
             return cameras.filter { camera -> camera.second.get(CameraCharacteristics.LENS_FACING) == facing }
                 .map { camera -> camera.first }
         }
 
-        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
         private fun listSupportedCodecs(list: MediaCodecList) {
             try {
                 for (codecInfo in list.codecInfos) {
@@ -862,7 +936,14 @@ class CameraService internal constructor(c: Context) {
             codec.setCallback(callback, handler)
         }
 
-        private fun chooseOptimalSize(choices: Array<Size>?, textureViewWidth: Int, textureViewHeight: Int, maxWidth: Int, maxHeight: Int, target: Size): Size {
+        private fun chooseOptimalSize(
+            choices: Array<Size>?,
+            textureViewWidth: Int,
+            textureViewHeight: Int,
+            maxWidth: Int,
+            maxHeight: Int,
+            target: Size
+        ): Size {
             if (choices == null) return target
             // Collect the supported resolutions that are at least as big as the preview Surface
             val bigEnough: MutableList<Size> = ArrayList()
diff --git a/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt b/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt
index 5ff08f52d..8bd6a922b 100644
--- a/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt
+++ b/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt
@@ -68,11 +68,8 @@ class HardwareServiceImpl(
     private val mAudioManager: AudioManager = mContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
     private var mBluetoothWrapper: BluetoothWrapper? = null
     private var currentFocus: AudioFocusRequestCompat? = null
-    private var mCapturingId: String? = null
-    private var mIsCapturing = false
-    private var mIsScreenSharing = false
     private var pendingScreenSharingSession: MediaProjection? = null
-    private var mShouldCapture = false
+    private val shouldCapture = HashSet<String>()
     private var mShouldSpeakerphone = false
     private val mHasSpeakerPhone: Boolean = hasSpeakerphone()
     private var mIsChoosePlugin = false
@@ -376,34 +373,6 @@ class HardwareServiceImpl(
         cameraService.setParameters(camId, format, width, height, rate, windowManager.defaultDisplay.rotation)
     }
 
-    fun startScreenShare(projection: MediaProjection, surface: TextureView): Boolean {
-        Log.d(TAG, "startScreenShare")
-        return if (!mIsScreenSharing && projection != null) {
-            mIsScreenSharing = true
-            projection.registerCallback(object : MediaProjection.Callback() {
-                override fun onStop() {
-                    stopScreenShare()
-                }
-            }, cameraService.videoHandler)
-            if (!cameraService.startScreenSharing(projection, mContext.resources.displayMetrics)) {
-                mIsScreenSharing = false
-                projection.stop()
-                return false
-            }
-            true
-        } else {
-            false
-        }
-    }
-
-    override fun stopScreenShare() {
-        if (mIsScreenSharing) {
-            cameraService.stopScreenSharing()
-            mIsScreenSharing = false
-            if (mShouldCapture) startCapture(mCapturingId)
-        }
-    }
-
     override fun startMediaHandler(mediaHandlerId: String?) {
         mIsChoosePlugin = true
         mMediaHandlerId = mediaHandlerId
@@ -419,21 +388,17 @@ class HardwareServiceImpl(
     }
 
     override fun startCapture(camId: String?) {
-        Log.w(TAG, "startCapture: $camId")
-        mShouldCapture = true
-        if (mIsCapturing && mCapturingId != null && mCapturingId == camId) {
-            return
-        }
-        val cam = camId ?: mCapturingId ?: cameraService.switchInput(true)
-        val videoParams = cameraService.getParams(cam)
-        if (videoParams == null) {
-            Log.w(TAG, "startCapture: no video parameters ")
+        val cam = camId ?: cameraService.switchInput(true) ?: return
+        Log.w(TAG, "DEBUG startCapture: $camId $cam")
+        shouldCapture.add(cam)
+        val videoParams = cameraService.getParams(cam) ?: return
+        if (videoParams.isCapturing) {
             return
         }
         val surface = mCameraPreviewSurface.get()
         if (surface == null) {
             Log.w(TAG, "Can't start capture: no surface registered.")
-            cameraService.setPreviewParams(videoParams)
+            //cameraService.setPreviewParams(videoParams)
             videoEvents.onNext(VideoEvent(start = true))
             return
         }
@@ -449,14 +414,17 @@ class HardwareServiceImpl(
                 videoParams.codec = null
             }
         }
-        Log.w(TAG, "startCapture: cam " + cam + " " + videoParams.codec + " useHardwareCodec:" + useHardwareCodec + " bitrate:" + mPreferenceService.bitrate)
-        mIsCapturing = true
-        mCapturingId = videoParams.id
-        Log.d(TAG, "startCapture: openCamera " + videoParams.id + " " + videoParams.width + "x" + videoParams.height + " rot" + videoParams.rotation)
+        Log.w(TAG,
+            "startCapture: id:$cam codec:${videoParams.codec} size:${videoParams.width}x${videoParams.height} rot${videoParams.rotation} hw:$useHardwareCodec bitrate:${mPreferenceService.bitrate}"
+        )
+        videoParams.isCapturing = true
 
-        if (mCapturingId == CameraService.VideoDevices.SCREEN_SHARING) {
-            startScreenShare(pendingScreenSharingSession!!, surface)
+        if (videoParams.id == CameraService.VideoDevices.SCREEN_SHARING) {
+            val projection = pendingScreenSharingSession ?: return
             pendingScreenSharingSession = null
+            if (!cameraService.startScreenSharing(videoParams, projection, surface, mContext.resources.displayMetrics)) {
+                projection.stop()
+            }
             return
         }
 
@@ -478,7 +446,7 @@ class HardwareServiceImpl(
                     }
 
                     override fun onError() {
-                        stopCapture(cam ?: "")
+                        stopCapture(videoParams.id)
                     }
                 },
                 useHardwareCodec,
@@ -486,7 +454,6 @@ class HardwareServiceImpl(
                 mPreferenceService.bitrate
             )
         }
-        cameraService.setPreviewParams(videoParams)
         videoEvents.onNext(VideoEvent(
             started = true,
             w = videoParams.width,
@@ -496,27 +463,17 @@ class HardwareServiceImpl(
     }
 
     override fun stopCapture(camId: String) {
-        Log.d(TAG, "stopCapture: " + cameraService.isOpen)
-        mShouldCapture = false
-        endCapture()
+        shouldCapture.remove(camId)
+        cameraService.closeCamera(camId)
+        videoEvents.onNext(VideoEvent(started = false))
     }
 
     override fun requestKeyFrame() {
         cameraService.requestKeyFrame()
     }
 
-    override fun setBitrate(device: String, bitrate: Int) {
-        cameraService.setBitrate(bitrate)
-    }
-
-    override fun endCapture() {
-        if (cameraService.isOpen) {
-            //final CameraService.VideoParams params = previewParams;
-            cameraService.closeCamera()
-            videoEvents.onNext(VideoEvent(started = false))
-        }
-        pendingScreenSharingSession = null
-        mIsCapturing = false
+    override fun setBitrate(camId: String, bitrate: Int) {
+        cameraService.setBitrate(camId, bitrate)
     }
 
     @Synchronized override fun addVideoSurface(id: String, holder: Any) {
@@ -558,27 +515,25 @@ class HardwareServiceImpl(
     @Synchronized override fun addPreviewVideoSurface(holder: Any, conference: Conference?) {
         if (holder !is TextureView)
             return
-        Log.w(TAG, "addPreviewVideoSurface " + holder.hashCode() + " mCapturingId " + mCapturingId)
         if (mCameraPreviewSurface.get() === holder) return
         mCameraPreviewSurface = WeakReference(holder)
         mCameraPreviewCall = WeakReference(conference)
-        if (mShouldCapture && !mIsCapturing) {
-            startCapture(mCapturingId)
-        }
+        for (c in shouldCapture)
+            startCapture(c)
     }
 
     @Synchronized override fun updatePreviewVideoSurface(conference: Conference) {
         val old = mCameraPreviewCall.get()
         mCameraPreviewCall = WeakReference(conference)
-        if (old !== conference && mIsCapturing) {
-            val id = mCapturingId
-            stopCapture(id ?:  "")
-            startCapture(id)
+        if (old !== conference) {
+            for (camId in shouldCapture) {
+                cameraService.closeCamera(camId)
+                startCapture(camId)
+            }
         }
     }
 
     @Synchronized override fun removeVideoSurface(id: String) {
-        Log.i(TAG, "removeVideoSurface $id")
         videoSurfaces.remove(id)
         val shm = videoInputs[id] ?: return
         if (shm.window != 0L) {
@@ -593,20 +548,19 @@ class HardwareServiceImpl(
     }
 
     override fun removePreviewVideoSurface() {
-        Log.w(TAG, "removePreviewVideoSurface")
         mCameraPreviewSurface.clear()
     }
 
     override fun switchInput(accountId:String, callId: String, setDefaultCamera: Boolean, screenCaptureSession: Any?) {
-        Log.w(TAG, "switchInput $callId")
-        mCapturingId = if (screenCaptureSession != null) {
+        Log.w(TAG, "DEBUG switchInput $callId $screenCaptureSession")
+        val camId = if (screenCaptureSession != null) {
             pendingScreenSharingSession = screenCaptureSession as MediaProjection
             CameraService.VideoDevices.SCREEN_SHARING
         } else {
             pendingScreenSharingSession = null
             cameraService.switchInput(setDefaultCamera)
         }
-        switchInput(accountId, callId, "camera://$mCapturingId")
+        switchInput(accountId, callId, "camera://$camId")
     }
 
     override fun setPreviewSettings() {
@@ -625,7 +579,7 @@ class HardwareServiceImpl(
 
     override fun setDeviceOrientation(rotation: Int) {
         cameraService.setOrientation(rotation)
-        mCapturingId?.let { id ->
+        /*mCapturingId?.let { id ->
             val videoParams = cameraService.getParams(id) ?: return
             videoEvents.onNext(VideoEvent(
                 started = true,
@@ -633,7 +587,7 @@ class HardwareServiceImpl(
                 h = videoParams.height,
                 rot = videoParams.rotation
             ))
-        }
+        }*/
     }
 
     override val videoDevices: List<String>
diff --git a/ring-android/libjamiclient/src/main/kotlin/net/jami/call/CallPresenter.kt b/ring-android/libjamiclient/src/main/kotlin/net/jami/call/CallPresenter.kt
index 621e56991..e2882e492 100644
--- a/ring-android/libjamiclient/src/main/kotlin/net/jami/call/CallPresenter.kt
+++ b/ring-android/libjamiclient/src/main/kotlin/net/jami/call/CallPresenter.kt
@@ -342,7 +342,7 @@ class CallPresenter @Inject constructor(
 
     fun previewVideoSurfaceDestroyed() {
         mHardwareService.removePreviewVideoSurface()
-        mHardwareService.endCapture()
+        //mHardwareService.endCapture()
     }
 
     fun uiVisibilityChanged(displayed: Boolean) {
diff --git a/ring-android/libjamiclient/src/main/kotlin/net/jami/services/DaemonService.kt b/ring-android/libjamiclient/src/main/kotlin/net/jami/services/DaemonService.kt
index 0c6c18757..edc5fc7ad 100644
--- a/ring-android/libjamiclient/src/main/kotlin/net/jami/services/DaemonService.kt
+++ b/ring-android/libjamiclient/src/main/kotlin/net/jami/services/DaemonService.kt
@@ -302,12 +302,12 @@ class DaemonService(
             mHardwareService.setParameters(camId, format, width, height, rate)
         }
 
-        fun requestKeyFrame() {
+        override fun requestKeyFrame(camId: String) {
             mHardwareService.requestKeyFrame()
         }
 
-        override fun setBitrate(device: String, bitrate: Int) {
-            mHardwareService.setBitrate(device, bitrate)
+        override fun setBitrate(camId: String, bitrate: Int) {
+            mHardwareService.setBitrate(camId, bitrate)
         }
 
         override fun startCapture(camId: String) {
diff --git a/ring-android/libjamiclient/src/main/kotlin/net/jami/services/HardwareService.kt b/ring-android/libjamiclient/src/main/kotlin/net/jami/services/HardwareService.kt
index ad0bff282..b1d1fd291 100644
--- a/ring-android/libjamiclient/src/main/kotlin/net/jami/services/HardwareService.kt
+++ b/ring-android/libjamiclient/src/main/kotlin/net/jami/services/HardwareService.kt
@@ -95,10 +95,8 @@ abstract class HardwareService(
     abstract fun startCapture(camId: String?)
     abstract fun stopCapture(camId: String)
     abstract fun hasMicrophone(): Boolean
-    abstract fun endCapture()
-    abstract fun stopScreenShare()
     abstract fun requestKeyFrame()
-    abstract fun setBitrate(device: String, bitrate: Int)
+    abstract fun setBitrate(camId: String, bitrate: Int)
     abstract fun addVideoSurface(id: String, holder: Any)
     abstract fun updateVideoSurfaceId(currentId: String, newId: String)
     abstract fun removeVideoSurface(id: String)
@@ -136,6 +134,7 @@ abstract class HardwareService(
     }
 
     fun startVideo(inputId: String, surface: Any, width: Int, height: Int): Long {
+        Log.i(TAG, "startVideo $inputId ${width}x$height")
         val inputWindow = JamiService.acquireNativeWindow(surface)
         if (inputWindow == 0L) {
             return inputWindow
@@ -146,6 +145,7 @@ abstract class HardwareService(
     }
 
     fun stopVideo(inputId: String, inputWindow: Long) {
+        Log.i(TAG, "stopVideo $inputId $inputWindow")
         if (inputWindow == 0L) {
             return
         }
@@ -165,11 +165,9 @@ abstract class HardwareService(
     @Synchronized
     fun startLogs(): Observable<String> {
         return logs ?: Observable.create(ObservableOnSubscribe { emitter: ObservableEmitter<String> ->
-            Log.w(TAG, "ObservableOnSubscribe JamiService.monitor(true)")
             logEmitter = emitter
             JamiService.monitor(true)
             emitter.setCancellable {
-                Log.w(TAG, "ObservableOnSubscribe CANCEL JamiService.monitor(false)")
                 synchronized(this@HardwareService) {
                     JamiService.monitor(false)
                     logEmitter = null
@@ -178,7 +176,7 @@ abstract class HardwareService(
             }
         } as ObservableOnSubscribe<String>)
             .observeOn(Schedulers.io())
-            .scan(StringBuffer(1024), { sb: StringBuffer, message: String? -> sb.append(message).append('\n') })
+            .scan(StringBuffer(1024)) { sb: StringBuffer, message: String -> sb.append(message).append('\n') }
             .throttleLatest(500, TimeUnit.MILLISECONDS)
             .map { obj: StringBuffer -> obj.toString() }
             .replay(1)
@@ -203,7 +201,7 @@ abstract class HardwareService(
     }
 
     companion object {
-        private val TAG = HardwareService::class.java.simpleName
+        private val TAG = HardwareService::class.simpleName!!
         val STATE_SPEAKERS = AudioState(AudioOutput.SPEAKERS)
         val STATE_INTERNAL = AudioState(AudioOutput.INTERNAL)
     }
-- 
GitLab