diff --git a/core/src/main/java/io/github/thibaultbee/streampack/data/VideoConfig.kt b/core/src/main/java/io/github/thibaultbee/streampack/data/VideoConfig.kt index b65d44f11..8e77ae830 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/data/VideoConfig.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/data/VideoConfig.kt @@ -16,6 +16,7 @@ package io.github.thibaultbee.streampack.data import android.content.Context +import android.media.MediaCodecInfo import android.media.MediaCodecInfo.CodecProfileLevel import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain8 import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline @@ -31,6 +32,7 @@ import android.media.MediaFormat import android.os.Build import android.util.Size import io.github.thibaultbee.streampack.internal.encoders.MediaCodecHelper +import io.github.thibaultbee.streampack.internal.utils.av.video.DynamicRangeProfile import io.github.thibaultbee.streampack.internal.utils.extensions.isDevicePortrait import io.github.thibaultbee.streampack.internal.utils.extensions.isVideo import io.github.thibaultbee.streampack.internal.utils.extensions.landscapize @@ -69,6 +71,9 @@ class VideoConfig( val fps: Int = 30, /** * Video encoder profile. Encoders may not support requested profile. In this case, StreamPack fallbacks to default profile. + * If not set, profile is always a 8 bit profile. StreamPack try to apply the highest profile available. + * If the decoder does not support the profile, you should explicitly set the profile to a lower + * value such as [AVCProfileBaseline] for AVC, [HEVCProfileMain] for HEVC, [VP9Profile0] for VP9. * ** See ** [MediaCodecInfo.CodecProfileLevel](https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel) */ profile: Int = getBestProfile(mimeType), @@ -132,6 +137,13 @@ class VideoConfig( gopDuration ) + /** + * The dynamic range profile. + * It is deduced from the [profile]. + * **See Also:** [DynamicRangeProfiles](https://developer.android.com/reference/android/hardware/camera2/params/DynamicRangeProfiles) + */ + val dynamicRangeProfile = DynamicRangeProfile.fromProfile(mimeType, profile) + /** * Get resolution according to device orientation * @@ -174,6 +186,23 @@ class VideoConfig( } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (dynamicRangeProfile != DynamicRangeProfile.sdr) { + format.setInteger( + MediaFormat.KEY_COLOR_STANDARD, + MediaFormat.COLOR_STANDARD_BT2020 + ) + format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED) + format.setInteger( + MediaFormat.KEY_COLOR_TRANSFER, + dynamicRangeProfile.transferFunction + ) + format.setFeatureEnabled( + MediaCodecInfo.CodecCapabilities.FEATURE_HdrEditing, true + ) + } + + } return format } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecEncoder.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecEncoder.kt index 7f4211b03..8f064c702 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecEncoder.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecEncoder.kt @@ -32,8 +32,10 @@ abstract class MediaCodecEncoder( EventHandler(), IEncoder { protected var mediaCodec: MediaCodec? = null set(value) { + if (value != null) { + onNewMediaCodec(value) + } field = value - onNewMediaCodec() } private var callbackThread: HandlerThread? = null private var handler: Handler? = null @@ -149,7 +151,7 @@ abstract class MediaCodecEncoder( } } - open fun onNewMediaCodec() {} + open fun onNewMediaCodec(mediaCodec: MediaCodec) {} open fun createMediaFormat(config: Config, withProfileLevel: Boolean) = config.getFormat(withProfileLevel) diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecHelper.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecHelper.kt index ee6021c1f..4071f7b41 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecHelper.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/MediaCodecHelper.kt @@ -241,7 +241,7 @@ object MediaCodecHelper { fun getProfiles( mimeType: String, ): List = - getProfileLevel(mimeType).map { it.profile } + getProfileLevel(mimeType).map { it.profile }.toSet().toList() /** * Get encoder supported profiles list for the specified encoder. @@ -254,7 +254,7 @@ object MediaCodecHelper { mimeType: String, name: String ): List = - getProfileLevel(mimeType, name).map { it.profile } + getProfileLevel(mimeType, name).map { it.profile }.toSet().toList() /** * Get encoder maximum supported levels for the default encoder. diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/VideoMediaCodecEncoder.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/VideoMediaCodecEncoder.kt index eca556b68..78883e139 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/VideoMediaCodecEncoder.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/encoders/VideoMediaCodecEncoder.kt @@ -29,6 +29,7 @@ import io.github.thibaultbee.streampack.internal.gl.EglWindowSurface import io.github.thibaultbee.streampack.internal.gl.FullFrameRect import io.github.thibaultbee.streampack.internal.gl.Texture2DProgram import io.github.thibaultbee.streampack.internal.interfaces.ISourceOrientationProvider +import io.github.thibaultbee.streampack.internal.utils.av.video.DynamicRangeProfile import io.github.thibaultbee.streampack.listeners.OnErrorListener import java.util.concurrent.Executors @@ -62,10 +63,17 @@ class VideoMediaCodecEncoder( _bitrate = value } - override fun onNewMediaCodec() { - mediaCodec?.let { - codecSurface?.outputSurface = it.createInputSurface() + override fun onNewMediaCodec(mediaCodec: MediaCodec) { + try { + val mimeType = mediaCodec.outputFormat.getString(MediaFormat.KEY_MIME)!! + val profile = mediaCodec.outputFormat.getInteger(MediaFormat.KEY_PROFILE) + codecSurface?.useHighBitDepth = + DynamicRangeProfile.fromProfile(mimeType, profile).isHdr + } catch (_: Exception) { + codecSurface?.useHighBitDepth = false } + + codecSurface?.outputSurface = mediaCodec.createInputSurface() } override fun createMediaFormat(config: Config, withProfileLevel: Boolean): MediaFormat { @@ -124,6 +132,11 @@ class VideoMediaCodecEncoder( val inputSurface: Surface? get() = surfaceTexture?.let { Surface(surfaceTexture) } + /** + * If true, the encoder will use high bit depth (10 bits) for encoding. + */ + var useHighBitDepth = false + var outputSurface: Surface? = null set(value) { /** @@ -145,7 +158,7 @@ class VideoMediaCodecEncoder( } private fun initOrUpdateSurfaceTexture(surface: Surface) { - eglSurface = ensureGlContext(EglWindowSurface(surface)) { + eglSurface = ensureGlContext(EglWindowSurface(surface, useHighBitDepth)) { val width = it.getWidth() val height = it.getHeight() val size = diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/gl/EglWindowSurface.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/gl/EglWindowSurface.kt index 319876c94..84b011a42 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/gl/EglWindowSurface.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/gl/EglWindowSurface.kt @@ -16,9 +16,14 @@ */ package io.github.thibaultbee.streampack.internal.gl -import android.opengl.* +import android.opengl.EGL14 +import android.opengl.EGLConfig +import android.opengl.EGLContext +import android.opengl.EGLDisplay +import android.opengl.EGLExt +import android.opengl.EGLSurface import android.view.Surface -import java.util.* +import java.util.Objects /** * Holds state associated with a Surface used for MediaCodec encoder input. @@ -30,7 +35,7 @@ import java.util.* * (Contains mostly code borrowed from CameraX) */ -class EglWindowSurface(private val surface: Surface) { +class EglWindowSurface(private val surface: Surface, useHighBitDepth: Boolean = false) { private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT private var eglSurface: EGLSurface = EGL14.EGL_NO_SURFACE @@ -41,13 +46,13 @@ class EglWindowSurface(private val surface: Surface) { } init { - eglSetup() + eglSetup(useHighBitDepth) } /** * Prepares EGL. We want a GLES 2.0 context and a surface that supports recording. */ - private fun eglSetup() { + private fun eglSetup(useHighBitDepth: Boolean) { eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) if (Objects.equals(eglDisplay, EGL14.EGL_NO_DISPLAY)) { throw RuntimeException("unable to get EGL14 display") @@ -59,12 +64,16 @@ class EglWindowSurface(private val surface: Surface) { // Configure EGL for recordable and OpenGL ES 2.0. We want enough RGB bits // to minimize artifacts from possible YUV conversion. + val eglColorSize = if (useHighBitDepth) 10 else 8 + val eglAlphaSize = if (useHighBitDepth) 2 else 0 + val recordable = if (useHighBitDepth) 0 else 1 var attribList = intArrayOf( - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_RED_SIZE, eglColorSize, + EGL14.EGL_GREEN_SIZE, eglColorSize, + EGL14.EGL_BLUE_SIZE, eglColorSize, + EGL14.EGL_ALPHA_SIZE, eglAlphaSize, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL_RECORDABLE_ANDROID, 1, + EGL_RECORDABLE_ANDROID, recordable, EGL14.EGL_NONE ) val numConfigs = IntArray(1) diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraController.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraController.kt index 7db30f46a..85405e91a 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraController.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraController.kt @@ -20,6 +20,7 @@ import android.content.Context import android.hardware.camera2.* import android.hardware.camera2.CameraDevice.AUDIO_RESTRICTION_NONE import android.hardware.camera2.CameraDevice.AUDIO_RESTRICTION_VIBRATION_SOUND +import android.hardware.camera2.params.OutputConfiguration import android.os.Build import android.util.Range import android.view.Surface @@ -110,9 +111,7 @@ class CameraController( private val captureCallback = object : CameraCaptureSession.CaptureCallback() { override fun onCaptureFailed( - session: CameraCaptureSession, - request: CaptureRequest, - failure: CaptureFailure + session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure ) { super.onCaptureFailed(session, request, failure) Logger.e(TAG, "Capture failed with code ${failure.reason}") @@ -121,25 +120,35 @@ class CameraController( @RequiresPermission(Manifest.permission.CAMERA) private suspend fun openCamera( - manager: CameraManager, - cameraId: String + manager: CameraManager, cameraId: String ): CameraDevice = suspendCancellableCoroutine { cont -> threadManager.openCamera( - manager, - cameraId, - CameraDeviceCallback(cont) + manager, cameraId, CameraDeviceCallback(cont) ) } private suspend fun createCaptureSession( camera: CameraDevice, - targets: List + targets: List, + dynamicRange: Long, ): CameraCaptureSession = suspendCancellableCoroutine { cont -> - threadManager.createCaptureSession( - camera, - targets, - CameraCaptureSessionCallback(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 + } + } + } + + threadManager.createCaptureSessionByOutputConfiguration( + camera, outputConfigurations, CameraCaptureSessionCallback(cont) + ) + } else { + threadManager.createCaptureSession( + camera, targets, CameraCaptureSessionCallback(cont) + ) + } } private fun createRequestSession( @@ -162,18 +171,17 @@ class CameraController( @RequiresPermission(Manifest.permission.CAMERA) suspend fun startCamera( cameraId: String, - targets: List + targets: List, + dynamicRange: Long, ) { require(targets.isNotEmpty()) { " At least one target is required" } withContext(coroutineDispatcher) { val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager camera = openCamera(manager, cameraId).also { cameraDevice -> - captureSession = - createCaptureSession( - cameraDevice, - targets - ) + captureSession = createCaptureSession( + cameraDevice, targets, dynamicRange + ) } } } @@ -183,13 +191,9 @@ class CameraController( require(captureSession != null) { "Capture session must not be null" } require(targets.isNotEmpty()) { " At least one target is required" } - captureRequest = - createRequestSession( - camera!!, - captureSession!!, - getClosestFpsRange(camera!!.id, fps), - targets - ) + captureRequest = createRequestSession( + camera!!, captureSession!!, getClosestFpsRange(camera!!.id, fps), targets + ) } fun stopCamera() { @@ -249,9 +253,7 @@ class CameraController( require(captureRequest != null) { "capture request must not be null" } threadManager.setRepeatingSingleRequest( - captureSession!!, - captureRequest!!.build(), - captureCallback + captureSession!!, captureRequest!!.build(), captureCallback ) } @@ -260,9 +262,7 @@ class CameraController( require(captureRequest != null) { "capture request must not be null" } threadManager.captureBurstRequests( - captureSession!!, - listOf(captureRequest!!.build()), - captureCallback + captureSession!!, listOf(captureRequest!!.build()), captureCallback ) } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraExecutorManager.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraExecutorManager.kt index 43cf0816a..075d7ab29 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraExecutorManager.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraExecutorManager.kt @@ -50,11 +50,19 @@ class CameraExecutorManager : ICameraThreadManager { targets: List, callback: CameraCaptureSession.StateCallback ) { - val outputs = mutableListOf() - targets.forEach { outputs.add(OutputConfiguration(it)) } + val outputConfigurations = targets.map { OutputConfiguration(it) } + createCaptureSessionByOutputConfiguration(camera, outputConfigurations, callback) + } + + @RequiresApi(Build.VERSION_CODES.P) + override fun createCaptureSessionByOutputConfiguration( + camera: CameraDevice, + outputConfigurations: List, + callback: CameraCaptureSession.StateCallback + ) { SessionConfiguration( SessionConfiguration.SESSION_REGULAR, - outputs, + outputConfigurations, cameraExecutor, callback ).also { sessionConfig -> diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraHandlerManager.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraHandlerManager.kt index 726beed50..e96cf6a63 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraHandlerManager.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraHandlerManager.kt @@ -20,6 +20,7 @@ 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.os.Handler import android.os.HandlerThread import android.view.Surface @@ -51,6 +52,19 @@ class CameraHandlerManager : ICameraThreadManager { camera.createCaptureSession(targets, callback, cameraHandler) } + override fun createCaptureSessionByOutputConfiguration( + camera: CameraDevice, + outputConfigurations: List, + callback: CameraCaptureSession.StateCallback + ) { + @Suppress("deprecation") + camera.createCaptureSessionByOutputConfigurations( + outputConfigurations, + callback, + cameraHandler + ) + } + override fun setRepeatingSingleRequest( captureSession: CameraCaptureSession, captureRequest: CaptureRequest, diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraSource.kt index b56baff3b..f1b57ec65 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraSource.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/CameraSource.kt @@ -24,6 +24,7 @@ import io.github.thibaultbee.streampack.data.VideoConfig import io.github.thibaultbee.streampack.internal.data.Frame import io.github.thibaultbee.streampack.internal.interfaces.ISourceOrientationProvider import io.github.thibaultbee.streampack.internal.sources.IVideoSource +import io.github.thibaultbee.streampack.internal.utils.av.video.DynamicRangeProfile import io.github.thibaultbee.streampack.internal.utils.extensions.deviceOrientation import io.github.thibaultbee.streampack.internal.utils.extensions.isDevicePortrait import io.github.thibaultbee.streampack.internal.utils.extensions.landscapize @@ -72,11 +73,13 @@ class CameraSource( } private var fps: Int = 30 + private var dynamicRangeProfile: DynamicRangeProfile = DynamicRangeProfile.sdr private var isStreaming = false private var isPreviewing = false override fun configure(config: VideoConfig) { this.fps = config.fps + this.dynamicRangeProfile = config.dynamicRangeProfile } @RequiresPermission(Manifest.permission.CAMERA) @@ -84,7 +87,7 @@ class CameraSource( var targets = mutableListOf() previewSurface?.let { targets.add(it) } encoderSurface?.let { targets.add(it) } - cameraController.startCamera(cameraId, targets) + cameraController.startCamera(cameraId, targets, dynamicRangeProfile.dynamicRange) targets = mutableListOf() previewSurface?.let { targets.add(it) } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/ICameraThreadManager.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/ICameraThreadManager.kt index 2827a9ce5..0173a1375 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/ICameraThreadManager.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/sources/camera/ICameraThreadManager.kt @@ -19,6 +19,7 @@ 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 /** @@ -40,10 +41,10 @@ interface ICameraThreadManager { ) /** - * Create a camera capture session. + * Create a camera capture session for surfaces. * * @param camera the [CameraDevice] - * @param targets list of surfaces + * @param targets list of [Surface] * @param callback an implementation of [CameraCaptureSession.StateCallback] */ fun createCaptureSession( @@ -52,6 +53,19 @@ interface ICameraThreadManager { 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, + callback: CameraCaptureSession.StateCallback + ) + /** * Set a repeating request. * diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/DynamicRangeProfile.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/DynamicRangeProfile.kt new file mode 100644 index 000000000..8ac24dcf4 --- /dev/null +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/DynamicRangeProfile.kt @@ -0,0 +1,91 @@ +package io.github.thibaultbee.streampack.internal.utils.av.video + +import android.hardware.camera2.params.DynamicRangeProfiles +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10 +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10 +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10Plus +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain8 +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedBaseline +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedHigh +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileExtended +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh10 +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh422 +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh444 +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileMain +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10 +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10 +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile0 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile1 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile3 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR10Plus +import android.media.MediaFormat + +data class DynamicRangeProfile(val dynamicRange: Long, val transferFunction: Int) { + val isHdr: Boolean + get() = dynamicRange != DynamicRangeProfiles.STANDARD + + companion object { + val sdr = + DynamicRangeProfile(DynamicRangeProfiles.STANDARD, MediaFormat.COLOR_TRANSFER_SDR_VIDEO) + val hdr = DynamicRangeProfile(DynamicRangeProfiles.HLG10, MediaFormat.COLOR_TRANSFER_HLG) + val hdr10 = + DynamicRangeProfile(DynamicRangeProfiles.HDR10, MediaFormat.COLOR_TRANSFER_ST2084) + val hdr10Plus = + DynamicRangeProfile(DynamicRangeProfiles.HDR10_PLUS, MediaFormat.COLOR_TRANSFER_ST2084) + + private val avcProfilesMap = mapOf( + AVCProfileBaseline to sdr, + AVCProfileConstrainedBaseline to sdr, + AVCProfileConstrainedHigh to sdr, + AVCProfileExtended to sdr, + AVCProfileHigh to sdr, + AVCProfileHigh10 to hdr, + AVCProfileHigh422 to sdr, + AVCProfileHigh444 to sdr, + AVCProfileMain to sdr, + ) + + private val hevcProfilesMap = mapOf( + HEVCProfileMain to sdr, + HEVCProfileMain10 to hdr, + HEVCProfileMain10HDR10 to hdr10, + HEVCProfileMain10HDR10Plus to hdr10Plus, + ) + + private val vp9ProfilesMap = mapOf( + VP9Profile0 to sdr, + VP9Profile1 to hdr, + VP9Profile2 to hdr, + VP9Profile2HDR to hdr, + VP9Profile2HDR10Plus to hdr10Plus, + VP9Profile3 to hdr, + VP9Profile3HDR to hdr10, + VP9Profile3HDR10Plus to hdr10Plus, + ) + + private val av1ProfilesMap = mapOf( + AV1ProfileMain8 to sdr, + AV1ProfileMain10 to hdr, + AV1ProfileMain10HDR10 to hdr10, + AV1ProfileMain10HDR10Plus to hdr10Plus, + ) + + fun fromProfile(mimetype: String, profile: Int): DynamicRangeProfile { + return when (mimetype) { + MediaFormat.MIMETYPE_VIDEO_AVC -> avcProfilesMap[profile] + MediaFormat.MIMETYPE_VIDEO_HEVC -> hevcProfilesMap[profile] + MediaFormat.MIMETYPE_VIDEO_VP9 -> vp9ProfilesMap[profile] + MediaFormat.MIMETYPE_VIDEO_AV1 -> av1ProfilesMap[profile] + else -> throw IllegalArgumentException("Unknown mimetype $mimetype") + } ?: throw IllegalArgumentException("Profile $profile is not supported for $mimetype") + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/Profile.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/Profile.kt index 3630ab67a..b35fb59ab 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/Profile.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/Profile.kt @@ -15,7 +15,11 @@ enum class Profile(val value: Int) { MediaCodecInfo.CodecProfileLevel.VP9Profile0 -> PROFILE_0 MediaCodecInfo.CodecProfileLevel.VP9Profile1 -> PROFILE_1 MediaCodecInfo.CodecProfileLevel.VP9Profile2 -> PROFILE_2 + MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR -> PROFILE_2 + MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus -> PROFILE_2 MediaCodecInfo.CodecProfileLevel.VP9Profile3 -> PROFILE_3 + MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR -> PROFILE_3 + MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR10Plus -> PROFILE_3 else -> throw IllegalArgumentException("Unknown profile: $mediaCodecProfile") } } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/VPCodecConfigurationRecord.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/VPCodecConfigurationRecord.kt index db7cd1108..cabd8d300 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/VPCodecConfigurationRecord.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/vpx/VPCodecConfigurationRecord.kt @@ -18,6 +18,7 @@ package io.github.thibaultbee.streampack.internal.utils.av.video.vpx import android.media.MediaFormat import android.os.Build import io.github.thibaultbee.streampack.internal.utils.av.buffer.ByteBufferWriter +import io.github.thibaultbee.streampack.internal.utils.av.video.DynamicRangeProfile import io.github.thibaultbee.streampack.internal.utils.extensions.put import io.github.thibaultbee.streampack.internal.utils.extensions.putShort import io.github.thibaultbee.streampack.internal.utils.extensions.shl @@ -90,7 +91,8 @@ data class VPCodecConfigurationRecord( * ``` */ fun fromMediaFormat(format: MediaFormat): VPCodecConfigurationRecord { - val profile = Profile.fromMediaFormat(format.getInteger(MediaFormat.KEY_PROFILE)) + val rawProfile = format.getInteger(MediaFormat.KEY_PROFILE) + val profile = Profile.fromMediaFormat(rawProfile) val level = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Level.fromMediaFormat(format.getInteger(MediaFormat.KEY_LEVEL)) @@ -98,7 +100,17 @@ data class VPCodecConfigurationRecord( Level.UNDEFINED // 0 is undefined } - val bitDepth = 8 // 8 bits - field is 8 or 10 or 12 bits + val mimeType = format.getString(MediaFormat.KEY_MIME)!! + // field is 8 or 10 (12 bits not supported on Android) + val bitDepth = if (DynamicRangeProfile.fromProfile( + mimeType, + rawProfile + ).isHdr + ) { + 10 + } else { + 8 + } val chromaSubsampling = try { ChromaSubsampling.fromValue( diff --git a/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/CameraStreamerConfigurationHelper.kt b/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/CameraStreamerConfigurationHelper.kt index d4cdd0378..41c4d9291 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/CameraStreamerConfigurationHelper.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/CameraStreamerConfigurationHelper.kt @@ -23,7 +23,9 @@ import io.github.thibaultbee.streampack.internal.muxers.IVideoMuxerHelper import io.github.thibaultbee.streampack.internal.muxers.flv.FlvMuxerHelper import io.github.thibaultbee.streampack.internal.muxers.mp4.MP4MuxerHelper import io.github.thibaultbee.streampack.internal.muxers.ts.TSMuxerHelper +import io.github.thibaultbee.streampack.internal.utils.av.video.DynamicRangeProfile import io.github.thibaultbee.streampack.streamers.bases.BaseCameraStreamer +import io.github.thibaultbee.streampack.utils.get10BitSupportedProfiles import io.github.thibaultbee.streampack.utils.getCameraFpsList import io.github.thibaultbee.streampack.utils.getCameraOutputStreamSizes @@ -77,4 +79,30 @@ class VideoCameraStreamerConfigurationHelper(muxerHelper: IVideoMuxerHelper) : val encoderFpsRange = super.getSupportedFramerate(mimeType) return context.getCameraFpsList(cameraId).filter { encoderFpsRange.contains(it) } } + + /** + * Get supported 8-bit and 10-bit profiles for a [BaseCameraStreamer]. + * + * @param context application context + * @param mimeType video encoder mime type + * @param cameraId camera id + * @return list of profiles + */ + fun getSupportedAllProfiles( + context: Context, + mimeType: String, + cameraId: String + ): List { + val supportedDynamicRangeProfiles = context.get10BitSupportedProfiles(cameraId) + + // If device doesn't support 10-bit, return all supported 8-bit profiles + return super.getSupportedAllProfiles(mimeType).filter { + supportedDynamicRangeProfiles.contains( + DynamicRangeProfile.fromProfile( + mimeType, + it + ).dynamicRange + ) + } + } } \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/StreamerConfigurationHelper.kt b/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/StreamerConfigurationHelper.kt index 366e5c891..76e009a39 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/StreamerConfigurationHelper.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/streamers/helpers/StreamerConfigurationHelper.kt @@ -16,7 +16,26 @@ package io.github.thibaultbee.streampack.streamers.helpers import android.media.AudioFormat -import android.media.MediaCodecInfo.CodecProfileLevel.* +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10 +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10 +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10Plus +import android.media.MediaCodecInfo.CodecProfileLevel.AV1ProfileMain8 +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedBaseline +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedHigh +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileExtended +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh10 +import android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileMain +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10 +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10 +import android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile0 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile1 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus import android.media.MediaFormat import android.util.Range import io.github.thibaultbee.streampack.internal.encoders.MediaCodecHelper @@ -26,6 +45,7 @@ import io.github.thibaultbee.streampack.internal.muxers.IVideoMuxerHelper import io.github.thibaultbee.streampack.internal.muxers.flv.FlvMuxerHelper import io.github.thibaultbee.streampack.internal.muxers.mp4.MP4MuxerHelper import io.github.thibaultbee.streampack.internal.muxers.ts.TSMuxerHelper +import io.github.thibaultbee.streampack.internal.utils.av.video.DynamicRangeProfile import io.github.thibaultbee.streampack.streamers.bases.BaseStreamer import java.security.InvalidParameterException @@ -165,13 +185,13 @@ open class VideoStreamerConfigurationHelper(private val videoMuxerHelper: IVideo ) = MediaCodecHelper.Video.getFramerateRange(mimeType) /** - * Get supported profiles for a [BaseStreamer]. + * Get supported 8-bit and 10-bit profiles for a [BaseStreamer]. * Removes profiles for 10 bits and still images. * * @param mimeType video encoder mime type * @return list of profile */ - fun getSupportedProfiles(mimeType: String): List { + fun getSupportedAllProfiles(mimeType: String): List { val profiles = when (mimeType) { MediaFormat.MIMETYPE_VIDEO_AVC -> avcProfiles MediaFormat.MIMETYPE_VIDEO_HEVC -> hevcProfiles @@ -180,29 +200,72 @@ open class VideoStreamerConfigurationHelper(private val videoMuxerHelper: IVideo else -> throw InvalidParameterException("Unknown mimetype $mimeType") } val supportedProfiles = MediaCodecHelper.getProfiles(mimeType) - return supportedProfiles.filter { profiles.contains(it) } + return profiles.filter { supportedProfiles.contains(it) } } + /** + * Get supported SDR (8-bit only) profiles for a [BaseStreamer]. + * Removes profiles for 10 bits and still images. + * + * @param mimeType video encoder mime type + * @return list of profile + */ + @Deprecated( + "Use [getSdrProfilesSupported] instead", + ReplaceWith("getSdrProfilesSupported(mimeType)") + ) + fun getSupportedProfiles(mimeType: String) = getSupportedSdrProfiles(mimeType) + + /** + * Get supported SDR (8-bit only) profiles for a [BaseStreamer]. + * Removes profiles for 10 bits and still images. + * + * @param mimeType video encoder mime type + * @return list of profile + */ + fun getSupportedSdrProfiles(mimeType: String): List { + val supportedProfiles = getSupportedAllProfiles(mimeType) + return supportedProfiles.filter { + !DynamicRangeProfile.fromProfile( + mimeType, + it + ).isHdr + } + } + + /** + * Only 420 profiles (8 and 10 bits) are supported. + */ private val avcProfiles = listOf( AVCProfileBaseline, AVCProfileConstrainedBaseline, AVCProfileConstrainedHigh, AVCProfileExtended, AVCProfileHigh, - AVCProfileMain + AVCProfileHigh10, + AVCProfileMain, ) private val hevcProfiles = listOf( - HEVCProfileMain + HEVCProfileMain, + HEVCProfileMain10, + HEVCProfileMain10HDR10, + HEVCProfileMain10HDR10Plus ) private val vp9Profiles = listOf( VP9Profile0, - VP9Profile1 + VP9Profile1, + VP9Profile2, + VP9Profile2HDR, + VP9Profile2HDR10Plus ) private val av1Profiles = listOf( - AV1ProfileMain8 + AV1ProfileMain8, + AV1ProfileMain10, + AV1ProfileMain10HDR10, + AV1ProfileMain10HDR10Plus ) } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt b/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt index 825d58e00..79b3ec240 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt @@ -19,7 +19,9 @@ import android.content.Context import android.graphics.ImageFormat import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraManager +import android.hardware.camera2.CameraMetadata import android.hardware.camera2.CaptureResult +import android.hardware.camera2.params.DynamicRangeProfiles.STANDARD import android.os.Build import android.util.Range import android.util.Size @@ -337,4 +339,49 @@ fun Context.getCameraOutputStreamSizes( fun Context.getCameraFpsList(cameraId: String): List> { return this.getCameraCharacteristics(cameraId)[CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES]?.toList() ?: emptyList() -} \ No newline at end of file +} + +/** + * Whether the camera supports the capability. + * + * @param cameraId camera id + * @return true if the camera supports the capability, false otherwise + */ +private fun Context.isCapabilitiesSupported(cameraId: String, capability: Int): Boolean { + val availableCapabilities = this.getCameraCharacteristics(cameraId) + .get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) + return availableCapabilities?.contains(capability) ?: false +} + +/** + * Whether the camera supports 10-bit dynamic range output. + * + * @param cameraId camera id + * @return true if the camera supports 10-bit dynamic range output, false otherwise + */ +fun Context.is10BitProfileSupported(cameraId: String): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + isCapabilitiesSupported( + cameraId, + CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT + ) + } else { + false + } +} + +/** + * Get list of 10-bit dynamic range output profiles. + * + * @param cameraId camera id + * @return List of 10-bit dynamic range output profiles + */ +fun Context.get10BitSupportedProfiles(cameraId: String): Set { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + this.getCameraCharacteristics(cameraId) + .get(CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES)?.supportedProfiles + ?: emptySet() + } else { + setOf(STANDARD) + } +} diff --git a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt index 7c0ff4844..10dd5d3b1 100644 --- a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt +++ b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt @@ -244,7 +244,11 @@ class SettingsFragment : PreferenceFragmentCompat() { } // Inflates profile - val profiles = streamerHelper.video.getSupportedProfiles(encoder) + val profiles = streamerHelper.video.getSupportedAllProfiles( + requireContext(), + encoder, + requireContext().defaultCameraId + ) .map { profileLevelDisplay.getProfileName( encoder, diff --git a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/ProfileLevelDisplay.kt b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/ProfileLevelDisplay.kt index b0b7daf4e..8483e3b64 100644 --- a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/ProfileLevelDisplay.kt +++ b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/ProfileLevelDisplay.kt @@ -117,7 +117,11 @@ import android.media.MediaCodecInfo.CodecProfileLevel.VP9Level62 import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile0 import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile1 import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile3 +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR +import android.media.MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR10Plus import android.media.MediaFormat import android.os.Build import io.github.thibaultbee.streampack.app.R @@ -202,7 +206,17 @@ class ProfileLevelDisplay(private val context: Context) { put(VP9Profile0, context.getString(R.string.video_profile_vp9_profile0)) put(VP9Profile1, context.getString(R.string.video_profile_vp9_profile1)) put(VP9Profile2, context.getString(R.string.video_profile_vp9_profile2)) + put(VP9Profile2HDR, context.getString(R.string.video_profile_vp9_profile2_hdr10)) + put( + VP9Profile2HDR10Plus, + context.getString(R.string.video_profile_vp9_profile2_hrd10plus) + ) put(VP9Profile3, context.getString(R.string.video_profile_vp9_profile3)) + put(VP9Profile3HDR, context.getString(R.string.video_profile_vp9_profile3_hdr10)) + put( + VP9Profile3HDR10Plus, + context.getString(R.string.video_profile_vp9_profile3_hdr10plus) + ) } } diff --git a/demos/camera/src/main/res/values/strings.xml b/demos/camera/src/main/res/values/strings.xml index 3f8545550..dcc0e97fb 100644 --- a/demos/camera/src/main/res/values/strings.xml +++ b/demos/camera/src/main/res/values/strings.xml @@ -137,7 +137,11 @@ Profile 0 Profile 1 Profile 2 + Profile 2 HDR10 + Profile 2 HDR10Plus Profile 3 + Profile 3 HDR10 + Profile 3 HDR10Plus Level video_level_key