Skip to content

Commit bde634e

Browse files
committed
Add StreamConfig support checking and address review comments
1 parent 70fa0fa commit bde634e

7 files changed

Lines changed: 252 additions & 84 deletions

File tree

core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt

Lines changed: 194 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,39 @@ import android.app.Application
1919
import android.content.ContentResolver
2020
import android.graphics.SurfaceTexture
2121
import android.net.Uri
22+
import android.util.Log
2223
import android.view.Surface
2324
import androidx.annotation.GuardedBy
2425
import androidx.concurrent.futures.DirectExecutor
2526
import androidx.test.ext.junit.runners.AndroidJUnit4
2627
import androidx.test.filters.LargeTest
2728
import androidx.test.platform.app.InstrumentationRegistry
2829
import androidx.test.rule.GrantPermissionRule
30+
import com.google.common.truth.Truth.assertThat
31+
import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.DYNAMIC_RANGE_HLG10
32+
import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.FPS_60
33+
import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.IMAGE_FORMAT_JPEG_ULTRA_HDR
34+
import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.STABILIZATION_MODE_ON
35+
import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.STREAM_CONFIG_SINGLE
36+
import com.google.jetpackcamera.core.camera.CameraXCameraSystemTest.Feature.VIDEO_QUALITY_UHD
2937
import com.google.jetpackcamera.core.camera.OnVideoRecordEvent.OnVideoRecordError
3038
import com.google.jetpackcamera.core.camera.OnVideoRecordEvent.OnVideoRecorded
3139
import com.google.jetpackcamera.core.camera.utils.APP_REQUIRED_PERMISSIONS
3240
import com.google.jetpackcamera.core.common.FakeFilePathGenerator
3341
import com.google.jetpackcamera.model.DynamicRange
3442
import com.google.jetpackcamera.model.FlashMode
3543
import com.google.jetpackcamera.model.Illuminant
44+
import com.google.jetpackcamera.model.ImageOutputFormat
3645
import com.google.jetpackcamera.model.LensFacing
3746
import com.google.jetpackcamera.model.SaveLocation
47+
import com.google.jetpackcamera.model.StabilizationMode
48+
import com.google.jetpackcamera.model.StreamConfig
3849
import com.google.jetpackcamera.model.VideoQuality
3950
import com.google.jetpackcamera.settings.ConstraintsRepository
4051
import com.google.jetpackcamera.settings.SettableConstraintsRepository
4152
import com.google.jetpackcamera.settings.SettableConstraintsRepositoryImpl
4253
import com.google.jetpackcamera.settings.model.CameraAppSettings
54+
import com.google.jetpackcamera.settings.model.CameraConstraints
4355
import com.google.jetpackcamera.settings.model.CameraSystemConstraints
4456
import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
4557
import java.io.File
@@ -80,6 +92,7 @@ class CameraXCameraSystemTest {
8092
private const val GENERAL_TIMEOUT_MS = 3_000L
8193
private const val RECORDING_TIMEOUT_MS = 10_000L
8294
private const val RECORDING_START_DURATION_MS = 500L
95+
private const val TAG = "CameraXCameraSystemTest"
8396
}
8497

8598
@get:Rule
@@ -171,65 +184,129 @@ class CameraXCameraSystemTest {
171184
}
172185

173186
@Test
174-
fun setMultipleFeatures_systemConstraintsUpdatedAndFeaturesSettableIfSupported() = runBlocking {
175-
// Arrange.
176-
val constraintsRepository = ObservableConstraintsRepository()
177-
val cameraSystem = createAndInitCameraXCameraSystem(
178-
constraintsRepository = constraintsRepository
187+
fun setMultipleFeatures_systemConstraintsUpdatedAndFeaturesSetIfSupported() = runBlocking {
188+
// TODO: Add STREAM_CONFIG_SINGLE to the featuresToTest list. This currently leads to flaky
189+
// crashes due to some camera effect related surface not being cleaned up properly somehow.
190+
// This doesn't seem to be related to the primary purpose of this test, so simply excluding
191+
// it for now.
192+
val featuresToTest = listOf(
193+
DYNAMIC_RANGE_HLG10,
194+
FPS_60,
195+
VIDEO_QUALITY_UHD
179196
)
180197

181-
// Each camera run/update should lead to a new systemConstraints update
182-
var systemConstraints = constraintsRepository.observeNextUpdate()
183-
cameraSystem.runCameraOnMain()
198+
featuresToTest.permutations().forEach { orderedFeatures ->
199+
Log.d(TAG, "Testing $orderedFeatures")
184200

185-
val lensFacing = cameraSystem.getCurrentSettings().value?.cameraLensFacing
201+
// Setup
202+
val constraintsRepository = ObservableConstraintsRepository()
203+
val cameraSystem = createAndInitCameraXCameraSystem(
204+
constraintsRepository = constraintsRepository
205+
)
186206

187-
// Act: For each of the features — HDR, 60 FPS, UHD recording, await previous constraints
188-
// update and set the feature if the constraints supports it.
207+
// Initial run: each camera run/update should lead to a new systemConstraints update
208+
var currentConstraints = constraintsRepository.observeNextUpdate().let {
209+
cameraSystem.runCameraOnMain()
210+
it.awaitUntil()
211+
}
189212

190-
if (
191-
systemConstraints
192-
.awaitUntil()
193-
.perLensConstraints[lensFacing]
194-
?.supportedDynamicRanges
195-
?.contains(DynamicRange.HLG10) == true
196-
) {
197-
systemConstraints = constraintsRepository.observeNextUpdate()
198-
cameraSystem.setDynamicRange(DynamicRange.HLG10)
199-
}
213+
val lensFacing =
214+
requireNotNull(cameraSystem.getCurrentSettings().value?.cameraLensFacing)
215+
216+
orderedFeatures.forEach { feature ->
217+
currentConstraints = when (feature) {
218+
DYNAMIC_RANGE_HLG10 -> feature.tryApplyFeature(
219+
expectedValue = DynamicRange.HLG10,
220+
lensFacing = lensFacing,
221+
cameraSystemConstraints = currentConstraints,
222+
constraintsRepository = constraintsRepository,
223+
cameraSystem = cameraSystem,
224+
setFeature = { cameraSystem.setDynamicRange(DynamicRange.HLG10) },
225+
getNewFeatureValue = { it?.dynamicRange }
226+
) { constraints ->
227+
constraints
228+
?.supportedDynamicRanges
229+
?.contains(DynamicRange.HLG10) == true
230+
}
200231

201-
if (
202-
systemConstraints
203-
.awaitUntil()
204-
.perLensConstraints[lensFacing]
205-
?.supportedFixedFrameRates
206-
?.contains(60) == true
207-
) {
208-
systemConstraints = constraintsRepository.observeNextUpdate()
209-
cameraSystem.setTargetFrameRate(60)
210-
}
232+
FPS_60 -> feature.tryApplyFeature(
233+
expectedValue = 60,
234+
lensFacing = lensFacing,
235+
cameraSystemConstraints = currentConstraints,
236+
constraintsRepository = constraintsRepository,
237+
cameraSystem = cameraSystem,
238+
setFeature = { cameraSystem.setTargetFrameRate(60) },
239+
getNewFeatureValue = { it?.targetFrameRate }
240+
) { constraints ->
241+
constraints
242+
?.supportedFixedFrameRates
243+
?.contains(60) == true
244+
}
211245

212-
if (
213-
systemConstraints
214-
.awaitUntil()
215-
.perLensConstraints[lensFacing]
216-
?.supportedVideoQualitiesMap
217-
?.get(cameraSystem.getCurrentSettings().value?.dynamicRange)
218-
?.contains(VideoQuality.UHD) == true
219-
) {
220-
systemConstraints = constraintsRepository.observeNextUpdate()
221-
cameraSystem.setVideoQuality(VideoQuality.UHD)
222-
}
246+
VIDEO_QUALITY_UHD -> feature.tryApplyFeature(
247+
expectedValue = VideoQuality.UHD,
248+
lensFacing = lensFacing,
249+
cameraSystemConstraints = currentConstraints,
250+
constraintsRepository = constraintsRepository,
251+
cameraSystem = cameraSystem,
252+
setFeature = { cameraSystem.setVideoQuality(VideoQuality.UHD) },
253+
getNewFeatureValue = { it?.videoQuality }
254+
) { constraints ->
255+
constraints
256+
?.supportedVideoQualitiesMap
257+
?.get(cameraSystem.getCurrentSettings().value?.dynamicRange)
258+
?.contains(VideoQuality.UHD) == true
259+
}
223260

224-
// Wait to ensure the async updateSystemConstraintsByFeatureGroups has time to run
225-
// and potentially crash if there's an issue.
226-
systemConstraints.awaitUntil()
261+
STABILIZATION_MODE_ON -> feature.tryApplyFeature(
262+
expectedValue = StabilizationMode.ON,
263+
lensFacing = lensFacing,
264+
cameraSystemConstraints = currentConstraints,
265+
constraintsRepository = constraintsRepository,
266+
cameraSystem = cameraSystem,
267+
setFeature = { cameraSystem.setStabilizationMode(StabilizationMode.ON) },
268+
getNewFeatureValue = { it?.stabilizationMode }
269+
) { constraints ->
270+
constraints
271+
?.supportedStabilizationModes
272+
?.contains(StabilizationMode.ON) == true
273+
}
227274

228-
// Assert.
229-
// If the test reaches here without crashing, it passes.
230-
// This ensures that the feature group logic doesn't cause runtime exceptions
231-
// even when high-end features are requested.
232-
return@runBlocking
275+
IMAGE_FORMAT_JPEG_ULTRA_HDR -> feature.tryApplyFeature(
276+
expectedValue = ImageOutputFormat.JPEG_ULTRA_HDR,
277+
lensFacing = lensFacing,
278+
cameraSystemConstraints = currentConstraints,
279+
constraintsRepository = constraintsRepository,
280+
cameraSystem = cameraSystem,
281+
setFeature = {
282+
cameraSystem.setImageFormat(
283+
ImageOutputFormat.JPEG_ULTRA_HDR
284+
)
285+
},
286+
getNewFeatureValue = { it?.imageFormat }
287+
) { constraints ->
288+
constraints
289+
?.supportedImageFormatsMap
290+
?.get(cameraSystem.getCurrentSettings().value?.streamConfig)
291+
?.contains(ImageOutputFormat.JPEG_ULTRA_HDR) == true
292+
}
293+
294+
STREAM_CONFIG_SINGLE -> feature.tryApplyFeature(
295+
expectedValue = StreamConfig.SINGLE_STREAM,
296+
lensFacing = lensFacing,
297+
cameraSystemConstraints = currentConstraints,
298+
constraintsRepository = constraintsRepository,
299+
cameraSystem = cameraSystem,
300+
setFeature = { cameraSystem.setStreamConfig(StreamConfig.SINGLE_STREAM) },
301+
getNewFeatureValue = { it?.streamConfig }
302+
) { constraints ->
303+
constraints
304+
?.supportedStreamConfigs
305+
?.contains(StreamConfig.SINGLE_STREAM) == true
306+
}
307+
}
308+
}
309+
}
233310
}
234311

235312
suspend fun <T> Deferred<T>.awaitUntil(timeout: Duration = 2.seconds): T {
@@ -285,7 +362,8 @@ class CameraXCameraSystemTest {
285362
getCurrentCameraState().transform { cameraState ->
286363
(cameraState.videoRecordingState as? VideoRecordingState.Active)?.let {
287364
emit(
288-
it.elapsedTimeNanos.toDuration(DurationUnit.NANOSECONDS).inWholeMilliseconds
365+
it.elapsedTimeNanos
366+
.toDuration(DurationUnit.NANOSECONDS).inWholeMilliseconds
289367
)
290368
}
291369
}.first { elapsedTimeMs ->
@@ -334,6 +412,71 @@ class CameraXCameraSystemTest {
334412
}
335413
}
336414

415+
suspend fun <T> Feature.tryApplyFeature(
416+
expectedValue: T,
417+
lensFacing: LensFacing,
418+
cameraSystemConstraints: CameraSystemConstraints,
419+
constraintsRepository: ObservableConstraintsRepository,
420+
cameraSystem: CameraSystem,
421+
setFeature: suspend () -> Unit,
422+
getNewFeatureValue: (CameraAppSettings?) -> T?,
423+
isSupported: (CameraConstraints?) -> Boolean
424+
): CameraSystemConstraints {
425+
// Check support
426+
if (!isSupported(cameraSystemConstraints.perLensConstraints[lensFacing])) {
427+
Log.d(TAG, "Skipping $this: Not supported by current constraints.")
428+
return cameraSystemConstraints
429+
}
430+
431+
Log.d(TAG, "Applying $this...")
432+
433+
// Prepare observer
434+
val nextUpdate = constraintsRepository.observeNextUpdate()
435+
436+
setFeature()
437+
438+
// Wait to verify constraints is updated
439+
val newConstraints = nextUpdate.awaitUntil()
440+
441+
// Verify feature is set according to current settings
442+
assertThat(getNewFeatureValue(cameraSystem.getCurrentSettings().value)).isEqualTo(
443+
expectedValue
444+
)
445+
446+
return newConstraints
447+
}
448+
449+
fun <T> List<T>.permutations(): List<List<T>> {
450+
if (isEmpty()) {
451+
// Base case: an empty list has one permutation (the empty list itself)
452+
return listOf(emptyList())
453+
}
454+
455+
val result = mutableListOf<List<T>>()
456+
val head = first() // Take the first element
457+
val tail = drop(1) // Get the rest of the list
458+
459+
// Recursively get permutations of the tail
460+
tail.permutations().forEach { permOfTail ->
461+
// Insert the head element at all possible positions in each permutation of the tail
462+
for (i in 0..permOfTail.size) {
463+
val newPerm = permOfTail.toMutableList()
464+
newPerm.add(i, head)
465+
result.add(newPerm)
466+
}
467+
}
468+
return result
469+
}
470+
471+
enum class Feature {
472+
DYNAMIC_RANGE_HLG10,
473+
FPS_60,
474+
VIDEO_QUALITY_UHD,
475+
STABILIZATION_MODE_ON,
476+
IMAGE_FORMAT_JPEG_ULTRA_HDR,
477+
STREAM_CONFIG_SINGLE
478+
}
479+
337480
class ObservableConstraintsRepository : SettableConstraintsRepository {
338481
private val lock = Object()
339482

core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ import androidx.core.content.ContextCompat.checkSelfPermission
6868
import androidx.core.net.toFile
6969
import androidx.lifecycle.asFlow
7070
import com.google.jetpackcamera.core.camera.FeatureGroupData.ExplicitlyGroupable
71-
import com.google.jetpackcamera.core.camera.FeatureGroupData.InexplicitlyGroupable
72-
import com.google.jetpackcamera.core.camera.FeatureGroupData.Ungroupable
7371
import com.google.jetpackcamera.core.camera.effects.SingleSurfaceForcingEffect
7472
import com.google.jetpackcamera.core.common.FilePathGenerator
7573
import com.google.jetpackcamera.model.AspectRatio
@@ -628,7 +626,11 @@ internal suspend fun createSessionConfig(
628626
"Setting initial device rotation to ${initialTransientSettings.deviceRotation}"
629627
)
630628

631-
val features = sessionSettings.toGroupableFeatures() ?: emptySet()
629+
val features = if (sessionSettings.toFeatureGroupDataSet().isInvalid()) {
630+
emptySet()
631+
} else {
632+
sessionSettings.toGroupableFeatures()
633+
}
632634

633635
Log.d(TAG, "createSessionConfig: sessionSettings = $sessionSettings, features = $features")
634636

@@ -650,13 +652,10 @@ internal suspend fun createSessionConfig(
650652
/**
651653
* Creates a set of [GroupableFeature] from a [PerpetualSessionSettings.SingleCamera].
652654
*
653-
* Only the [PerpetualSessionSettings.SingleCamera] values that are supported by CameraX feature
654-
* group APIs are included in the returned set.
655-
*
656-
* A null value is returned if the feature groups API can't be used for some value in
657-
* [PerpetualSessionSettings.SingleCamera], e.g. optical stabilization, or 15 FPS.
655+
* Only the [PerpetualSessionSettings.SingleCamera] values that are compatible with CameraX feature
656+
* group APIs (i.e. [ExplicitlyGroupable] features) are included in the returned set.
658657
*/
659-
internal fun PerpetualSessionSettings.SingleCamera.toGroupableFeatures(): Set<GroupableFeature>? {
658+
internal fun PerpetualSessionSettings.SingleCamera.toGroupableFeatures(): Set<GroupableFeature> {
660659
return buildSet {
661660
this@toGroupableFeatures.toFeatureGroupDataSet().forEach {
662661
when (it) {
@@ -672,8 +671,7 @@ internal fun PerpetualSessionSettings.SingleCamera.toGroupableFeatures(): Set<Gr
672671
add(it.feature)
673672
}
674673
}
675-
is InexplicitlyGroupable -> {} // No-op.
676-
is Ungroupable -> return null
674+
else -> {} // No-op.
677675
}
678676
}
679677
}.toSet()

0 commit comments

Comments
 (0)