Skip to content

Commit

Permalink
Implementation of VideoCapture (without audio) (#8)
Browse files Browse the repository at this point in the history
* ImageCapture

* Implement VideoCapture usecase.

Start recording video on a long press on the capture button, stop when released.
  • Loading branch information
yasith committed Jun 20, 2023
1 parent 0738ef4 commit bda8af0
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 13 deletions.
2 changes: 1 addition & 1 deletion domain/camera/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dependencies {
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'

// CameraX
def camerax_version = "1.1.0-beta01"
def camerax_version = "1.3.0-alpha05"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ interface CameraUseCase {
)

suspend fun takePicture()

suspend fun startVideoRecording()

fun stopVideoRecording()
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package com.google.jetpackcamera.domain.camera

import android.app.Application
import android.content.ContentValues
import android.provider.MediaStore
import android.util.Log
import android.util.Rational
import androidx.camera.core.CameraSelector
Expand All @@ -28,10 +30,18 @@ import androidx.camera.core.Preview
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.ViewPort
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
import androidx.core.util.Consumer
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asExecutor
import java.util.Date
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import javax.inject.Inject
Expand All @@ -54,12 +64,18 @@ class CameraXCameraUseCase @Inject constructor(
private val previewUseCase = Preview.Builder()
.build()

private val recorder = Recorder.Builder().setExecutor(defaultDispatcher.asExecutor()).build()
private val videoCaptureUseCase = VideoCapture.withOutput(recorder)

private val useCaseGroup = UseCaseGroup.Builder()
.setViewPort(ViewPort.Builder(ASPECT_RATIO_16_9, previewUseCase.targetRotation).build())
.addUseCase(previewUseCase)
.addUseCase(imageCaptureUseCase)
.addUseCase(videoCaptureUseCase)
.build()

private var recording : Recording? = null

override suspend fun initialize(): List<Int> {
cameraProvider = ProcessCameraProvider.getInstance(application).await()

Expand All @@ -81,7 +97,6 @@ class CameraXCameraUseCase @Inject constructor(
Log.d(TAG, "startPreview")

val cameraSelector = cameraLensToSelector(lensFacing)

previewUseCase.setSurfaceProvider(surfaceProvider)

cameraProvider.runWith(cameraSelector, useCaseGroup) {
Expand All @@ -107,6 +122,31 @@ class CameraXCameraUseCase @Inject constructor(
})
}

override suspend fun startVideoRecording() {
Log.d(TAG, "recordVideo")
val name = "JCA-recording-${Date()}.mp4"
val contentValues = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, name)
}
val mediaStoreOutput = MediaStoreOutputOptions.Builder(application.contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()

recording = videoCaptureUseCase.output
.prepareRecording(application, mediaStoreOutput)
.start(ContextCompat.getMainExecutor(application), Consumer { videoRecordEvent ->
run {
Log.d(TAG, videoRecordEvent.toString())
}
})
}

override fun stopVideoRecording() {
Log.d(TAG, "stopRecording")
recording?.stop()
}

private fun cameraLensToSelector(@LensFacing lensFacing: Int): CameraSelector =
when (lensFacing) {
CameraSelector.LENS_FACING_FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class FakeCameraUseCase : CameraUseCase {
var previewStarted = false
var numPicturesTaken = 0

var recordingInProgress = false

override suspend fun initialize(): List<Int> {
initialized = true
return availableLenses
Expand All @@ -56,4 +58,12 @@ class FakeCameraUseCase : CameraUseCase {
}
numPicturesTaken += 1
}

override suspend fun startVideoRecording() {
recordingInProgress = true
}

override fun stopVideoRecording() {
recordingInProgress = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

package com.google.jetpackcamera.feature.preview

import android.provider.MediaStore.Video
import android.util.Log
import androidx.camera.core.Preview.SurfaceProvider
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
Expand All @@ -27,7 +29,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
Expand Down Expand Up @@ -123,21 +124,46 @@ fun PreviewScreen(
modifier = Modifier.align(Alignment.BottomCenter)
) {
CaptureButton(
onClick = { viewModel.captureImage() }
onClick = { viewModel.captureImage() },
onLongPress = { viewModel.startVideoRecording() },
onRelease = { viewModel.stopVideoRecording() },
state = previewUiState.videoRecordingState
)
}
}
}
}

@Composable
fun CaptureButton(onClick: () -> Unit) {
Button(
onClick = onClick,
shape = CircleShape,
fun CaptureButton(
onClick: () -> Unit,
onLongPress: () -> Unit,
onRelease: () -> Unit,
state: VideoRecordingState
) {
Box(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
onLongPress()
}, onPress = {
awaitRelease()
onRelease()
}, onTap = { onClick() })
}
.size(120.dp)
.padding(18.dp)
.border(4.dp, Color.White, CircleShape)
) {}
) {
Canvas(modifier = Modifier.size(110.dp), onDraw = {
drawCircle(
color =
when (state) {
VideoRecordingState.INACTIVE -> Color.Transparent
VideoRecordingState.ACTIVE -> Color.Red
}
)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,24 @@ import androidx.camera.core.CameraSelector
*/
data class PreviewUiState(
val cameraState: CameraState = CameraState.NOT_READY,
val lensFacing: Int = CameraSelector.LENS_FACING_FRONT
val lensFacing: Int = CameraSelector.LENS_FACING_FRONT,
val videoRecordingState: VideoRecordingState = VideoRecordingState.INACTIVE,
)

/**
* Defines the current state of Video Recording
*/
enum class VideoRecordingState {
/**
* Camera is not currently recording a video
*/
INACTIVE,
/**
* Camera is currently recording a video
*/
ACTIVE
}

/**
* Defines the current state of the camera.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ package com.google.jetpackcamera.feature.preview

import android.util.Log
import androidx.camera.core.ImageCaptureException
import androidx.lifecycle.LifecycleOwner
import androidx.camera.core.Preview.SurfaceProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.camera.core.Preview.SurfaceProvider
import com.google.jetpackcamera.domain.camera.CameraUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
Expand All @@ -46,6 +44,8 @@ class PreviewViewModel @Inject constructor(
val previewUiState: StateFlow<PreviewUiState> = _previewUiState
var runningCameraJob: Job? = null

private var recordingJob : Job? = null

init {
initializeCamera()
}
Expand Down Expand Up @@ -97,4 +97,36 @@ class PreviewViewModel @Inject constructor(
}
}
}

fun startVideoRecording() {
Log.d(TAG, "startVideoRecording")
recordingJob = viewModelScope.launch {

try {
cameraUseCase.startVideoRecording()
_previewUiState.emit(
previewUiState.value.copy(
videoRecordingState = VideoRecordingState.ACTIVE
)
)
Log.d(TAG, "cameraUseCase.startRecording success")
} catch (exception: IllegalStateException) {
Log.d(TAG, "cameraUseCase.startVideoRecording error")
Log.d(TAG, exception.toString())
}
}
}

fun stopVideoRecording() {
Log.d(TAG, "stopVideoRecording")
viewModelScope.launch {
_previewUiState.emit(
previewUiState.value.copy(
videoRecordingState = VideoRecordingState.INACTIVE
)
)
}
cameraUseCase.stopVideoRecording()
recordingJob?.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ class PreviewViewModelTest {
assertEquals(cameraUseCase.numPicturesTaken, 1)
}

@Test
fun startVideoRecording() = runTest(StandardTestDispatcher()) {
previewViewModel.runCamera(mock())
previewViewModel.startVideoRecording()
advanceUntilIdle()
assertEquals(cameraUseCase.recordingInProgress, true)
}

@Test
fun stopVideoRecording() = runTest(StandardTestDispatcher()) {
previewViewModel.runCamera(mock())
previewViewModel.startVideoRecording()
advanceUntilIdle()
previewViewModel.stopVideoRecording()
assertEquals(cameraUseCase.recordingInProgress, false)

}

@Test
fun flipCamera() {
// TODO(yasith)
Expand Down

0 comments on commit bda8af0

Please sign in to comment.