Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of VideoCapture (without audio) #8

Merged
merged 3 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
yasith marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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
yasith marked this conversation as resolved.
Show resolved Hide resolved
)
)
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()
yasith marked this conversation as resolved.
Show resolved Hide resolved
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