Skip to content

Commit

Permalink
VideoRecordingState refactor (#271)
Browse files Browse the repository at this point in the history
* Refactor VideoRecordingState to update directly from the camera instead of UI
* Move VideoRecordingState to CameraState
* VideoRecordingState.Inactive stores the duration of the most recent recording
* Convert VideoRecordingState.Active to interface, and implemented as Paused and Recording
* Audio amplitude, elapsed time, and max video duration are stored in VideoRecordingState.Active
  • Loading branch information
Kimblebee authored Oct 17, 2024
1 parent a55ecae commit f13c104
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ private fun setFlashModeInternal(
Log.d(TAG, "Set flash mode to: ${imageCapture.flashMode}")
}

context(CameraSessionContext)
private suspend fun startVideoRecordingInternal(
initialMuted: Boolean,
videoCaptureUseCase: VideoCapture<Recorder>,
Expand Down Expand Up @@ -527,48 +528,127 @@ private suspend fun startVideoRecordingInternal(
return pendingRecord.start(callbackExecutor) { onVideoRecordEvent ->
Log.d(TAG, onVideoRecordEvent.toString())
when (onVideoRecordEvent) {
is VideoRecordEvent.Start -> {
currentCameraState.update { old ->
old.copy(
videoRecordingState = VideoRecordingState.Active.Recording(
audioAmplitude = onVideoRecordEvent.recordingStats.audioStats
.audioAmplitude,
maxDurationMillis = maxDurationMillis,
elapsedTimeNanos = onVideoRecordEvent.recordingStats
.recordedDurationNanos
)
)
}
}

is VideoRecordEvent.Pause -> {
currentCameraState.update { old ->
old.copy(
videoRecordingState = VideoRecordingState.Active.Paused(
audioAmplitude = onVideoRecordEvent.recordingStats.audioStats
.audioAmplitude,
maxDurationMillis = maxDurationMillis,
elapsedTimeNanos = onVideoRecordEvent.recordingStats
.recordedDurationNanos
)
)
}
}

is VideoRecordEvent.Resume -> {
currentCameraState.update { old ->
old.copy(
videoRecordingState = VideoRecordingState.Active.Recording(
audioAmplitude = onVideoRecordEvent.recordingStats.audioStats
.audioAmplitude,
maxDurationMillis = maxDurationMillis,
elapsedTimeNanos = onVideoRecordEvent.recordingStats
.recordedDurationNanos
)
)
}
}

is VideoRecordEvent.Status -> {
currentCameraState.update { old ->
// don't want to change state from paused to recording if status changes while paused
if (old.videoRecordingState is VideoRecordingState.Active.Paused) {
old.copy(
videoRecordingState = VideoRecordingState.Active.Paused(
audioAmplitude = onVideoRecordEvent.recordingStats.audioStats
.audioAmplitude,
maxDurationMillis = maxDurationMillis,
elapsedTimeNanos = onVideoRecordEvent.recordingStats
.recordedDurationNanos
)
)
} else {
old.copy(
videoRecordingState = VideoRecordingState.Active.Recording(
audioAmplitude = onVideoRecordEvent.recordingStats.audioStats
.audioAmplitude,
maxDurationMillis = maxDurationMillis,
elapsedTimeNanos = onVideoRecordEvent.recordingStats
.recordedDurationNanos
)
)
}
}
}

is VideoRecordEvent.Finalize -> {
when (onVideoRecordEvent.error) {
ERROR_NONE -> {
// update recording state to inactive with the final values of the recording.
currentCameraState.update { old ->
old.copy(
videoRecordingState = VideoRecordingState.Inactive(
finalElapsedTimeNanos = onVideoRecordEvent.recordingStats
.recordedDurationNanos
)
)
}
onVideoRecord(
CameraUseCase.OnVideoRecordEvent.OnVideoRecorded(
onVideoRecordEvent.outputResults.outputUri,
onVideoRecordEvent.recordingStats.recordedDurationNanos
onVideoRecordEvent.outputResults.outputUri
)
)
}

ERROR_DURATION_LIMIT_REACHED -> {
currentCameraState.update { old ->
old.copy(
videoRecordingState = VideoRecordingState.Inactive(
// cleanly display the max duration
finalElapsedTimeNanos = maxDurationMillis * 1_000_000
)
)
}

onVideoRecord(
CameraUseCase.OnVideoRecordEvent.OnVideoRecorded(
onVideoRecordEvent.outputResults.outputUri,
(maxDurationMillis * 1_000_000) // cleanly display the max duration
onVideoRecordEvent.outputResults.outputUri
)
)
}
else ->

else -> {
onVideoRecord(
CameraUseCase.OnVideoRecordEvent.OnVideoRecordError(
onVideoRecordEvent.cause
)
)
}
}
}

is VideoRecordEvent.Status -> {
onVideoRecord(
CameraUseCase.OnVideoRecordEvent.OnVideoRecordStatus(
audioAmplitude = onVideoRecordEvent.recordingStats.audioStats
.audioAmplitude,
elapsedTimeNanos = onVideoRecordEvent.recordingStats.recordedDurationNanos
)
)
}
}
}.apply {
mute(initialMuted)
}
}

context(CameraSessionContext)
private suspend fun runVideoRecording(
camera: Camera,
videoCapture: VideoCapture<Recorder>,
Expand Down Expand Up @@ -668,7 +748,6 @@ internal suspend fun processVideoControlEvents(
"Attempted video recording with null videoCapture"
)
}

recordingJob = launch(start = CoroutineStart.UNDISPATCHED) {
runVideoRecording(
camera,
Expand All @@ -683,7 +762,6 @@ internal suspend fun processVideoControlEvents(
)
}
}

VideoCaptureControlEvent.StopRecordingEvent -> {
recordingJob?.cancel()
recordingJob = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,9 @@ interface CameraUseCase {
/**
* Represents the events for video recording.
*/
sealed interface OnVideoRecordEvent {
data class OnVideoRecorded(
val savedUri: Uri,
val finalDurationNanos: Long
) : OnVideoRecordEvent

data class OnVideoRecordStatus(
val audioAmplitude: Double,
val elapsedTimeNanos: Long
) : OnVideoRecordEvent
sealed interface OnVideoRecordEvent {
data class OnVideoRecorded(val savedUri: Uri) : OnVideoRecordEvent

data class OnVideoRecordError(val error: Throwable?) : OnVideoRecordEvent
}
Expand All @@ -156,7 +149,39 @@ interface CameraUseCase {
}
}

sealed interface VideoRecordingState {

/**
* Camera is not currently recording a video
*/
data class Inactive(
val finalElapsedTimeNanos: Long = 0
) : VideoRecordingState

/**
* Camera is currently active; paused, stopping, or recording a video
*/
sealed interface Active : VideoRecordingState {
val maxDurationMillis: Long
val audioAmplitude: Double
val elapsedTimeNanos: Long

data class Recording(
override val maxDurationMillis: Long,
override val audioAmplitude: Double,
override val elapsedTimeNanos: Long
) : Active

data class Paused(
override val maxDurationMillis: Long,
override val audioAmplitude: Double,
override val elapsedTimeNanos: Long
) : Active
}
}

data class CameraState(
val videoRecordingState: VideoRecordingState = VideoRecordingState.Inactive(),
val zoomScale: Float = 1f,
val sessionFirstFrameTimestamp: Long = 0L,
val torchEnabled: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.tracing.Trace
import com.google.jetpackcamera.core.camera.VideoRecordingState
import com.google.jetpackcamera.feature.preview.quicksettings.QuickSettingsScreenOverlay
import com.google.jetpackcamera.feature.preview.ui.CameraControlsOverlay
import com.google.jetpackcamera.feature.preview.ui.PreviewDisplay
Expand Down Expand Up @@ -322,9 +323,7 @@ private fun ContentScreenPreview() {
private fun ContentScreen_WhileRecording() {
MaterialTheme(colorScheme = darkColorScheme()) {
ContentScreen(
previewUiState = FAKE_PREVIEW_UI_STATE_READY.copy(
videoRecordingState = VideoRecordingState.ACTIVE
),
previewUiState = FAKE_PREVIEW_UI_STATE_READY.copy(),
screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
surfaceRequest = null
)
Expand All @@ -333,6 +332,7 @@ private fun ContentScreen_WhileRecording() {

private val FAKE_PREVIEW_UI_STATE_READY = PreviewUiState.Ready(
currentCameraSettings = DEFAULT_CAMERA_APP_SETTINGS,
videoRecordingState = VideoRecordingState.Inactive(),
systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS,
previewMode = PreviewMode.StandardMode {},
captureModeToggleUiState = CaptureModeToggleUiState.Invisible
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.jetpackcamera.feature.preview

import com.google.jetpackcamera.core.camera.VideoRecordingState
import com.google.jetpackcamera.feature.preview.ui.SnackbarData
import com.google.jetpackcamera.feature.preview.ui.ToastMessage
import com.google.jetpackcamera.settings.model.CameraAppSettings
Expand All @@ -31,11 +32,9 @@ sealed interface PreviewUiState {
val currentCameraSettings: CameraAppSettings,
val systemConstraints: SystemConstraints,
val zoomScale: Float = 1f,
val videoRecordingState: VideoRecordingState = VideoRecordingState.INACTIVE,
val videoRecordingState: VideoRecordingState,
val quickSettingsIsOpen: Boolean = false,
val audioAmplitude: Double = 0.0,
val audioMuted: Boolean = false,
val recordingElapsedTimeNanos: Long = 0L,

// todo: remove after implementing post capture screen
val toastMessageToShow: ToastMessage? = null,
Expand All @@ -50,17 +49,3 @@ sealed interface PreviewUiState {
) : PreviewUiState
}
// todo(kc): add ElapsedTimeUiState class
/**
* 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class PreviewViewModel @AssistedInject constructor(
systemConstraints,
cameraAppSettings
),
videoRecordingState = cameraState.videoRecordingState,
isDebugMode = isDebugMode,
currentLogicalCameraId = cameraState.debugInfo.logicalCameraId,
currentPhysicalCameraId = cameraState.debugInfo.physicalCameraId
Expand All @@ -154,6 +155,7 @@ class PreviewViewModel @AssistedInject constructor(
PreviewUiState.Ready(
currentCameraSettings = cameraAppSettings,
systemConstraints = systemConstraints,
videoRecordingState = cameraState.videoRecordingState,
zoomScale = cameraState.zoomScale,
sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp,
previewMode = previewMode,
Expand Down Expand Up @@ -641,13 +643,10 @@ class PreviewViewModel @AssistedInject constructor(
val cookie = "Video-${videoCaptureStartedCount.incrementAndGet()}"
try {
cameraUseCase.startVideoRecording(videoCaptureUri, shouldUseUri) {
var audioAmplitude = 0.0
var timer = 0L
var snackbarToShow: SnackbarData? = null
when (it) {
is CameraUseCase.OnVideoRecordEvent.OnVideoRecorded -> {
Log.d(TAG, "cameraUseCase.startRecording OnVideoRecorded")
timer = it.finalDurationNanos
onVideoCapture(VideoCaptureEvent.VideoSaved(it.savedUri))
snackbarToShow = SnackbarData(
cookie = cookie,
Expand All @@ -667,28 +666,16 @@ class PreviewViewModel @AssistedInject constructor(
testTag = VIDEO_CAPTURE_FAILURE_TAG
)
}

is CameraUseCase.OnVideoRecordEvent.OnVideoRecordStatus -> {
audioAmplitude = it.audioAmplitude
timer = it.elapsedTimeNanos
}
}

viewModelScope.launch {
_previewUiState.update { old ->
(old as? PreviewUiState.Ready)?.copy(
snackBarToShow = snackbarToShow,
audioAmplitude = audioAmplitude,
recordingElapsedTimeNanos = timer
snackBarToShow = snackbarToShow
) ?: old
}
}
}
_previewUiState.update { old ->
(old as? PreviewUiState.Ready)?.copy(
videoRecordingState = VideoRecordingState.ACTIVE
) ?: old
}
Log.d(TAG, "cameraUseCase.startRecording success")
} catch (exception: IllegalStateException) {
Log.d(TAG, "cameraUseCase.startVideoRecording error", exception)
Expand All @@ -698,13 +685,6 @@ class PreviewViewModel @AssistedInject constructor(

fun stopVideoRecording() {
Log.d(TAG, "stopVideoRecording")
viewModelScope.launch {
_previewUiState.update { old ->
(old as? PreviewUiState.Ready)?.copy(
videoRecordingState = VideoRecordingState.INACTIVE
) ?: old
}
}
cameraUseCase.stopVideoRecording()
recordingJob?.cancel()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import com.google.jetpackcamera.core.camera.VideoRecordingState
import com.google.jetpackcamera.feature.preview.CaptureModeToggleUiState
import com.google.jetpackcamera.feature.preview.PreviewMode
import com.google.jetpackcamera.feature.preview.PreviewUiState
Expand Down Expand Up @@ -300,6 +301,7 @@ fun ExpandedQuickSettingsUiPreview() {
currentCameraSettings = CameraAppSettings(),
systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS,
previewMode = PreviewMode.StandardMode {},
videoRecordingState = VideoRecordingState.Inactive(),
captureModeToggleUiState = CaptureModeToggleUiState.Invisible
),
currentCameraSettings = CameraAppSettings(),
Expand All @@ -326,7 +328,8 @@ fun ExpandedQuickSettingsUiPreview_WithHdr() {
currentCameraSettings = CameraAppSettings(),
systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS,
previewMode = PreviewMode.StandardMode {},
captureModeToggleUiState = CaptureModeToggleUiState.Invisible
captureModeToggleUiState = CaptureModeToggleUiState.Invisible,
videoRecordingState = VideoRecordingState.Inactive()
),
currentCameraSettings = CameraAppSettings(dynamicRange = DynamicRange.HLG10),
onLensFaceClick = { },
Expand Down
Loading

0 comments on commit f13c104

Please # to comment.