Skip to content

Commit

Permalink
Bump cameraX to 1.3.1
Browse files Browse the repository at this point in the history
- Add new audio config feature to video capture
- Add video capture quality selector
- Add set effects method for camera
  • Loading branch information
ujizin committed Dec 17, 2023
1 parent ec27335 commit aff50c1
Show file tree
Hide file tree
Showing 14 changed files with 283 additions and 140 deletions.
6 changes: 3 additions & 3 deletions buildSrc/src/main/kotlin/ujizin/camposer/Config.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.ujizin.camposer
package ujizin.camposer

object Config {
const val compileSdk = 33
const val targetSdk = 33
const val compileSdk = 34
const val targetSdk = 34
const val minSdk = 21
const val versionCode = 8
const val versionName = "0.3.2"
Expand Down
2 changes: 1 addition & 1 deletion camposer/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import com.ujizin.camposer.Config
import ujizin.camposer.Config

plugins {
id("com.android.library")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ internal abstract class CameraTest {
}

private companion object {
private const val CAMERA_TIMEOUT = 2_500L
private const val CAMERA_TIMEOUT = 10_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package com.ujizin.camposer

import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.camera.core.ImageProxy
import androidx.camera.video.FileOutputOptions
import androidx.camera.view.video.AudioConfig
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
Expand All @@ -15,7 +16,6 @@ import com.ujizin.camposer.state.rememberImageAnalyzer
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
Expand All @@ -28,89 +28,99 @@ internal class CaptureModeTest : CameraTest() {
get() = InstrumentationRegistry.getInstrumentation().context

@Test
@Ignore("Flaky test, sometimes throw exception \"Camera closed\"")
fun test_captureMode() = with(composeTestRule) {
initCaptureModeCamera(CaptureMode.Image)

var isFinalized = false
runOnIdle {
val imageFile = File(context.filesDir, IMAGE_TEST_FILENAME).apply { createNewFile() }

cameraState.takePicture(imageFile) { result ->
when (result) {
is ImageCaptureResult.Error -> throw result.throwable
is ImageCaptureResult.Success -> {
assertEquals(Uri.fromFile(imageFile), result.savedUri)
assertEquals(CaptureMode.Image, cameraState.captureMode)
isFinalized = true
}
}
}
}

waitUntil(CAPTURE_MODE_TIMEOUT) { isFinalized }
}

@Test
fun test_videoCaptureMode() = with(composeTestRule) {
// No support for API Level 23 below
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return

initCaptureModeCamera(CaptureMode.Video)

if (!cameraState.isVideoSupported) return

// Create file
val videoFile = File(context.filesDir, VIDEO_TEST_FILENAME).apply {
delete()
createNewFile()
}

var isFinished = false
runOnIdle {
val videoFile = File(context.filesDir, VIDEO_TEST_FILENAME).apply { createNewFile() }
cameraState.startRecording(videoFile) { result ->
cameraState.startRecording(FileOutputOptions.Builder(videoFile).build(), AudioConfig.AUDIO_DISABLED) { result ->
when (result) {
is VideoCaptureResult.Error -> {
throw result.throwable ?: Exception(result.message)
}

is VideoCaptureResult.Error -> throw result.throwable ?: error(result.message)
is VideoCaptureResult.Success -> {
assertEquals(Uri.fromFile(videoFile), result.savedUri)
assertEquals(CaptureMode.Video, cameraState.captureMode)
isFinished = true
}
}
}
runBlocking {
delay(RECORD_VIDEO_DELAY)
cameraState.stopRecording()
}
}

runBlocking {
delay(RECORD_VIDEO_DELAY)
cameraState.stopRecording()
}

waitUntil(CAPTURE_MODE_TIMEOUT) { isFinished }
}

@Test
fun test_videoCaptureModeWithAnalysis() = with(composeTestRule) {
// No support for API Level 23 below
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
// Create file
val videoFile = File(context.filesDir, VIDEO_TEST_FILENAME).apply {
delete()
createNewFile()
}

var isAnalyzeCalled = false
initCaptureModeCamera(CaptureMode.Video) {
isAnalyzeCalled = true
}
initCaptureModeCamera(CaptureMode.Video) { isAnalyzeCalled = true }

if (!cameraState.isImageAnalysisSupported) return
if (!cameraState.isImageAnalysisSupported || !cameraState.isVideoSupported) return

runOnIdle {
val videoFile = File(context.filesDir, VIDEO_TEST_FILENAME).apply { createNewFile() }
var isFinished = false

cameraState.startRecording(videoFile) { result ->
runOnIdle {
cameraState.startRecording(FileOutputOptions.Builder(videoFile).build()) { result ->
when (result) {
is VideoCaptureResult.Error -> {
throw result.throwable ?: Exception(result.message)
}

is VideoCaptureResult.Error -> throw result.throwable ?: error(result.message)
is VideoCaptureResult.Success -> {
assertEquals(Uri.fromFile(videoFile), result.savedUri)
assertEquals(CaptureMode.Video, cameraState.captureMode)
if (cameraState.isImageAnalysisSupported) {
assertEquals(true, cameraState.isImageAnalysisEnabled)
assertEquals(true, isAnalyzeCalled)
}
isFinished = true
}
}
}
runBlocking {
delay(RECORD_VIDEO_DELAY)
cameraState.stopRecording()
}
}

runBlocking {
delay(RECORD_VIDEO_DELAY)
cameraState.stopRecording()
}

waitUntil(CAPTURE_MODE_TIMEOUT) { isFinished }
}

private fun ComposeContentTestRule.initCaptureModeCamera(
Expand All @@ -129,6 +139,7 @@ internal class CaptureModeTest : CameraTest() {

private companion object {
private const val RECORD_VIDEO_DELAY = 2000L
private const val CAPTURE_MODE_TIMEOUT = 10000L
private const val IMAGE_TEST_FILENAME = "capture_mode_test.jpg"
private const val VIDEO_TEST_FILENAME = "capture_mode_test.mp4"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal class FlashModeTest : CameraTest() {
initFlashCamera(camSelector = CamSelector.Back)
if (!cameraState.hasFlashUnit) return

FlashMode.values().forEach { mode ->
FlashMode.entries.forEach { mode ->
flashMode.value = mode
onNodeWithTag("${flashMode.value}").assertIsDisplayed()
runOnIdle { assertEquals(mode, cameraState.flashMode) }
Expand Down
6 changes: 6 additions & 0 deletions camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.ujizin.camposer
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.view.ViewGroup
import androidx.camera.video.QualitySelector
import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -51,6 +52,7 @@ import androidx.camera.core.CameraSelector as CameraXSelector
* @param isImageAnalysisEnabled enable or disable image analysis
* @param isFocusOnTapEnabled turn on feature focus on tap if true
* @param isPinchToZoomEnabled turn on feature pinch to zoom if true
* @param videoQualitySelector quality selector to the video capture
* @param onPreviewStreamChanged dispatch when preview is switching to front or back
* @param onSwitchToFront composable preview when change camera to front and it's not been streaming yet
* @param onSwitchToBack composable preview when change camera to back and it's not been streaming yet
Expand Down Expand Up @@ -79,6 +81,7 @@ public fun CameraPreview(
isImageAnalysisEnabled: Boolean = cameraState.isImageAnalysisEnabled,
isFocusOnTapEnabled: Boolean = cameraState.isFocusOnTapEnabled,
isPinchToZoomEnabled: Boolean = cameraState.isZoomSupported,
videoQualitySelector: QualitySelector = cameraState.videoQualitySelector,
onPreviewStreamChanged: () -> Unit = {},
onSwitchToFront: @Composable (Bitmap) -> Unit = {},
onSwitchToBack: @Composable (Bitmap) -> Unit = {},
Expand Down Expand Up @@ -107,6 +110,7 @@ public fun CameraPreview(
implementationMode = implementationMode,
isFocusOnTapEnabled = isFocusOnTapEnabled,
isPinchToZoomEnabled = isPinchToZoomEnabled,
videoQualitySelector = videoQualitySelector,
onZoomRatioChanged = onZoomRatioChanged,
focusTapContent = focusTapContent,
onFocus = onFocus,
Expand Down Expand Up @@ -136,6 +140,7 @@ internal fun CameraPreviewImpl(
isImageAnalysisEnabled: Boolean,
isFocusOnTapEnabled: Boolean,
isPinchToZoomEnabled: Boolean,
videoQualitySelector: QualitySelector,
onZoomRatioChanged: (Float) -> Unit,
onPreviewStreamChanged: () -> Unit,
onFocus: suspend (() -> Unit) -> Unit,
Expand Down Expand Up @@ -200,6 +205,7 @@ internal fun CameraPreviewImpl(
zoomRatio = zoomRatio,
imageCaptureMode = imageCaptureMode,
meteringPoint = meteringPointFactory.createPoint(x, y),
videoQualitySelector = videoQualitySelector,
exposureCompensation = exposureCompensation,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.ujizin.camposer.extensions

import android.Manifest
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.camera.core.ImageCapture
import androidx.camera.view.video.OutputFileOptions
import androidx.camera.video.FileDescriptorOutputOptions
import androidx.camera.video.FileOutputOptions
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.view.video.AudioConfig
import com.ujizin.camposer.state.CameraState
import com.ujizin.camposer.state.ImageCaptureResult
import com.ujizin.camposer.state.VideoCaptureResult
Expand Down Expand Up @@ -45,30 +50,44 @@ public suspend fun CameraState.takePicture(
/**
* Transform toggle recording file to suspend function
* */
@RequiresApi(Build.VERSION_CODES.M)
public suspend fun CameraState.toggleRecording(file: File): Uri? = suspendCancellableCoroutine { cont ->
with(cont) { toggleRecording(file, ::toggleRecordContinuation) }
}
@RequiresApi(Build.VERSION_CODES.O)
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
public suspend fun CameraState.toggleRecording(
fileOutputOptions: FileOutputOptions,
audioConfig: AudioConfig = AudioConfig.create(true),
): Uri? =
suspendCancellableCoroutine { cont ->
with(cont) { toggleRecording(fileOutputOptions, audioConfig, ::toggleRecordContinuation) }
}

/**
* Transform toggle recording content values options to suspend function
* */
@RequiresApi(Build.VERSION_CODES.M)
@RequiresApi(Build.VERSION_CODES.N)
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
public suspend fun CameraState.toggleRecording(
contentValues: ContentValues,
saveCollection: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
mediaStoreOutputOptions: MediaStoreOutputOptions,
audioConfig: AudioConfig = AudioConfig.create(true),
): Uri? = suspendCancellableCoroutine { cont ->
with(cont) { toggleRecording(contentValues, saveCollection, ::toggleRecordContinuation) }
with(cont) { toggleRecording(mediaStoreOutputOptions, audioConfig, ::toggleRecordContinuation) }
}

/**
* Transform toggle recording output files options to suspend function
* */
@RequiresApi(Build.VERSION_CODES.M)
@RequiresApi(Build.VERSION_CODES.O)
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
public suspend fun CameraState.toggleRecording(
outputFileOptions: OutputFileOptions
fileDescriptorOutputOptions: FileDescriptorOutputOptions,
audioConfig: AudioConfig = AudioConfig.create(true),
): Uri? = suspendCancellableCoroutine { cont ->
with(cont) { toggleRecording(outputFileOptions, ::toggleRecordContinuation) }
with(cont) {
toggleRecording(
fileDescriptorOutputOptions,
audioConfig,
::toggleRecordContinuation
)
}
}

private fun Continuation<Uri?>.takePictureContinuation(result: ImageCaptureResult) {
Expand Down
Loading

0 comments on commit aff50c1

Please sign in to comment.