Skip to content

Commit

Permalink
feat(core): add hfr support
Browse files Browse the repository at this point in the history
  • Loading branch information
ThibaultBee committed Dec 9, 2024
1 parent b3c975e commit e18fc4d
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ import android.hardware.camera2.CameraDevice.AUDIO_RESTRICTION_VIBRATION_SOUND
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.OutputConfiguration
import android.os.Build
import android.util.Range
import android.view.Surface
import androidx.annotation.RequiresPermission
import io.github.thibaultbee.streampack.core.error.CameraException
import io.github.thibaultbee.streampack.core.internal.sources.video.camera.dispatchers.CameraExecutorManager
import io.github.thibaultbee.streampack.core.internal.sources.video.camera.dispatchers.CameraHandlerManager
import io.github.thibaultbee.streampack.core.logger.Logger
import io.github.thibaultbee.streampack.core.utils.extensions.getAutoFocusModes
import io.github.thibaultbee.streampack.core.utils.extensions.getCameraFps
Expand Down Expand Up @@ -64,6 +65,7 @@ class CameraController(
}

private fun getClosestFpsRange(cameraId: String, fps: Int): Range<Int> {
return Range(fps, fps)
var fpsRangeList = context.getCameraFps(cameraId)
Logger.i(TAG, "Supported FPS range list: $fpsRangeList")

Expand Down Expand Up @@ -148,27 +150,11 @@ class CameraController(
}

private suspend fun createCaptureSession(
camera: CameraDevice,
targets: List<Surface>,
dynamicRange: Long,
camera: CameraDevice, targets: List<Surface>, isHfr: Boolean, dynamicRange: Long,
): CameraCaptureSession = suspendCancellableCoroutine { cont ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val outputConfigurations = targets.map {
OutputConfiguration(it).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
dynamicRangeProfile = dynamicRange
}
}
}

cameraDispatchManager.createCaptureSessionByOutputConfiguration(
camera, outputConfigurations, CameraCaptureSessionCallback(cont)
)
} else {
cameraDispatchManager.createCaptureSession(
camera, targets, CameraCaptureSessionCallback(cont)
)
}
cameraDispatchManager.createCaptureSession(
camera, isHfr, dynamicRange, targets, CameraCaptureSessionCallback(cont)
)
}

private fun createRequestSession(
Expand Down Expand Up @@ -198,6 +184,7 @@ class CameraController(
suspend fun startCamera(
cameraId: String,
targets: List<Surface>,
isHfr: Boolean,
dynamicRange: Long,
) {
require(targets.isNotEmpty()) { " At least one target is required" }
Expand All @@ -206,7 +193,7 @@ class CameraController(
val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
camera = openCamera(manager, cameraId).also { cameraDevice ->
captureSession = createCaptureSession(
cameraDevice, targets, dynamicRange
cameraDevice, targets, isHfr, dynamicRange
)
}
}
Expand All @@ -219,8 +206,9 @@ class CameraController(
val camera = requireNotNull(camera) { "Camera must not be null" }
val captureSession = requireNotNull(captureSession) { "Capture session must not be null" }

val fpsRange = getClosestFpsRange(camera.id, fps)
captureRequest = createRequestSession(
camera, captureSession, getClosestFpsRange(camera.id, fps), targets
camera, captureSession, fpsRange, targets
)
requestSessionSurface.addAll(targets)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import io.github.thibaultbee.streampack.core.internal.data.Frame
import io.github.thibaultbee.streampack.core.internal.utils.av.video.DynamicRangeProfile
import io.github.thibaultbee.streampack.core.logger.Logger
import io.github.thibaultbee.streampack.core.utils.extensions.defaultCameraId
import io.github.thibaultbee.streampack.core.utils.extensions.isConfigSupported
import io.github.thibaultbee.streampack.core.utils.extensions.isFpsHighSpeed
import io.github.thibaultbee.streampack.core.utils.extensions.isFrameRateSupported
import kotlinx.coroutines.runBlocking
import java.nio.ByteBuffer
Expand Down Expand Up @@ -77,8 +79,7 @@ class CameraSource(

override var cameraId: String = context.defaultCameraId
get() = cameraController.cameraId ?: field
@RequiresPermission(Manifest.permission.CAMERA)
set(value) {
@RequiresPermission(Manifest.permission.CAMERA) set(value) {
if (field == value) {
Logger.w(TAG, "Camera ID is already set to $value")
return
Expand Down Expand Up @@ -122,8 +123,8 @@ class CameraSource(
}

override fun configure(config: VideoConfig) {
if (!context.isFrameRateSupported(cameraId, config.fps)) {
Logger.w(TAG, "Camera $cameraId does not support ${config.fps} fps")
if (context.isConfigSupported(cameraId, config)) {
"Camera $cameraId does not support $config"
}

var needRestart = false
Expand Down Expand Up @@ -162,8 +163,9 @@ class CameraSource(
previewSurface?.let { targets.add(it) }
outputSurface?.let { targets.add(it) }

val isHfr = context.isFpsHighSpeed(cameraId, fps)
cameraController.startCamera(
cameraId, targets, dynamicRangeProfile.dynamicRange
cameraId, targets, isHfr, dynamicRangeProfile.dynamicRange
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.thibaultbee.streampack.core.internal.sources.video.camera
package io.github.thibaultbee.streampack.core.internal.sources.video.camera.dispatchers

import android.Manifest
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
Expand All @@ -29,39 +30,30 @@ import androidx.annotation.RequiresPermission
import java.util.concurrent.Executors

/**
* A [ICameraThreadManager] that manages camera API >= 28.
* A [ICameraDispatcherManager] that manages camera API >= 28.
*/
class CameraExecutorManager : ICameraThreadManager {
class CameraExecutorManager : ICameraDispatcherManager {
private val cameraExecutor = Executors.newSingleThreadExecutor()

@RequiresApi(Build.VERSION_CODES.P)
@RequiresPermission(Manifest.permission.CAMERA)
override fun openCamera(
manager: CameraManager,
cameraId: String,
callback: CameraDevice.StateCallback
manager: CameraManager, cameraId: String, callback: CameraDevice.StateCallback
) {
manager.openCamera(cameraId, cameraExecutor, callback)
}

@RequiresApi(Build.VERSION_CODES.P)
override fun createCaptureSession(
camera: CameraDevice,
isHfr: Boolean,
dynamicRange: Long,
targets: List<Surface>,
callback: CameraCaptureSession.StateCallback
) {
val outputConfigurations = targets.map { OutputConfiguration(it) }
createCaptureSessionByOutputConfiguration(camera, outputConfigurations, callback)
}

@RequiresApi(Build.VERSION_CODES.P)
override fun createCaptureSessionByOutputConfiguration(
camera: CameraDevice,
outputConfigurations: List<OutputConfiguration>,
callback: CameraCaptureSession.StateCallback
) {
SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
if (isHfr) SessionConfiguration.SESSION_HIGH_SPEED else SessionConfiguration.SESSION_REGULAR,
outputConfigurations,
cameraExecutor,
callback
Expand All @@ -76,7 +68,13 @@ class CameraExecutorManager : ICameraThreadManager {
captureRequest: CaptureRequest,
callback: CameraCaptureSession.CaptureCallback
): Int {
return captureSession.setSingleRepeatingRequest(captureRequest, cameraExecutor, callback)
return if (captureSession is CameraConstrainedHighSpeedCaptureSession) {
captureSession.setRepeatingBurstRequests(
captureSession.createHighSpeedRequestList(captureRequest), cameraExecutor, callback
)
} else {
captureSession.setSingleRepeatingRequest(captureRequest, cameraExecutor, callback)
}
}

@RequiresApi(Build.VERSION_CODES.P)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,67 +13,85 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.thibaultbee.streampack.core.internal.sources.video.camera
package io.github.thibaultbee.streampack.core.internal.sources.video.camera.dispatchers

import android.Manifest
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.DynamicRangeProfiles
import android.hardware.camera2.params.OutputConfiguration
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission

/**
* As camera API that support a [Handler] are deprecated since API >= 30.
* Is is a [ICameraThreadManager] that manages camera API < 30.
* Is is a [ICameraDispatcherManager] that manages camera API < 30.
*/
class CameraHandlerManager : ICameraThreadManager {
class CameraHandlerManager : ICameraDispatcherManager {
private var cameraThread = HandlerThread("CameraThread").apply { start() }
private var cameraHandler = Handler(cameraThread.looper)

@RequiresPermission(Manifest.permission.CAMERA)
override fun openCamera(
manager: CameraManager,
cameraId: String,
callback: CameraDevice.StateCallback
manager: CameraManager, cameraId: String, callback: CameraDevice.StateCallback
) {
manager.openCamera(cameraId, callback, cameraHandler)
}

override fun createCaptureSession(
camera: CameraDevice,
isHfr: Boolean,
dynamicRange: Long,
targets: List<Surface>,
callback: CameraCaptureSession.StateCallback
) {
@Suppress("deprecation")
camera.createCaptureSession(targets, callback, cameraHandler)
}

@RequiresApi(Build.VERSION_CODES.N)
override fun createCaptureSessionByOutputConfiguration(
camera: CameraDevice,
outputConfigurations: List<OutputConfiguration>,
callback: CameraCaptureSession.StateCallback
) {
@Suppress("deprecation")
camera.createCaptureSessionByOutputConfigurations(
outputConfigurations,
callback,
cameraHandler
)
require(DynamicRangeProfiles.STANDARD == dynamicRange) {
"Dynamic range profile $dynamicRange is not supported by this dispatcher API"
}
@Suppress("deprecation") if (isHfr) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
camera.createConstrainedHighSpeedCaptureSession(targets, callback, cameraHandler)
} else {
throw UnsupportedOperationException("High frame rate is not supported by this dispatcher API")
}
} else {
if (Build.VERSION_CODES.N <= Build.VERSION.SDK_INT) {
camera.createCaptureSession(
targets, callback, cameraHandler
)
} else {
val outputConfigurations = targets.map { OutputConfiguration(it) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
camera.createCaptureSessionByOutputConfigurations(
outputConfigurations, callback, cameraHandler
)
} else {
camera.createCaptureSession(
targets, callback, cameraHandler
)
}
}
}
}

override fun setRepeatingSingleRequest(
captureSession: CameraCaptureSession,
captureRequest: CaptureRequest,
callback: CameraCaptureSession.CaptureCallback
): Int {
return captureSession.setRepeatingRequest(captureRequest, callback, cameraHandler)
return if (captureSession is CameraConstrainedHighSpeedCaptureSession) {
captureSession.setRepeatingBurst(
captureSession.createHighSpeedRequestList(captureRequest), callback, cameraHandler
)
} else {
captureSession.setRepeatingRequest(captureRequest, callback, cameraHandler)
}
}

override fun captureBurstRequests(
Expand All @@ -92,6 +110,4 @@ class CameraHandlerManager : ICameraThreadManager {
e.printStackTrace()
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.thibaultbee.streampack.core.internal.sources.video.camera
package io.github.thibaultbee.streampack.core.internal.sources.video.camera.dispatchers

import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.OutputConfiguration
import android.view.Surface

/**
* Encapsulates camera2 API changes and deprecation.
*/
interface ICameraThreadManager {
interface ICameraDispatcherManager {

/**
* Opens camera device.
Expand All @@ -35,37 +34,26 @@ interface ICameraThreadManager {
* @param callback an implementation of [CameraDevice.StateCallback]
*/
fun openCamera(
manager: CameraManager,
cameraId: String,
callback: CameraDevice.StateCallback
manager: CameraManager, cameraId: String, callback: CameraDevice.StateCallback
)

/**
* Create a camera capture session for surfaces.
*
* @param camera the [CameraDevice]
* @param isHfr true if high frame rate is enabled
* @param dynamicRange the dynamic range profile
* @param targets list of [Surface]
* @param callback an implementation of [CameraCaptureSession.StateCallback]
*/
fun createCaptureSession(
camera: CameraDevice,
isHfr: Boolean,
dynamicRange: Long,
targets: List<Surface>,
callback: CameraCaptureSession.StateCallback
)

/**
* Create a camera capture session for output configurations.
*
* @param camera the [CameraDevice]
* @param outputConfigurations list of [OutputConfiguration]
* @param callback an implementation of [CameraCaptureSession.StateCallback]
*/
fun createCaptureSessionByOutputConfiguration(
camera: CameraDevice,
outputConfigurations: List<OutputConfiguration>,
callback: CameraCaptureSession.StateCallback
)

/**
* Set a repeating request.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.github.thibaultbee.streampack.core.internal.utils.extensions

import android.util.Range

fun <T> Iterable<List<T>>.unzip(): List<List<T>> {
val expectedSize = this.first().size
val result = MutableList(expectedSize) { mutableListOf<T>() }
Expand All @@ -28,3 +30,7 @@ fun <T> Iterable<List<T>>.unzip(): List<List<T>> {
}
return result
}

fun <T : Comparable<T>> List<Range<T>>.contains(value: T): Boolean {
return this.any { range -> range.contains(value) }
}
Loading

0 comments on commit e18fc4d

Please sign in to comment.