Video clipper #115
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces an Android application for automated video highlight generation and intelligent editing suggestions using Vertex AI for Firebase and Firebase Cloud Storage. The feedback highlights several critical compilation errors and logic issues, including the use of non-existent Media3 APIs (MetadataRetriever and EditedMediaItemSequence.withAudioAndVideoFrom), conflicting imports for PlayPauseButton, and a type mismatch in Long.coerceIn. Additionally, improvements are suggested to handle background thread operations for metadata retrieval, prevent stale state on screen re-initialization, validate user intent input, and safely abort processing if any video upload fails.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| import androidx.media3.common.util.UnstableApi | ||
| import androidx.media3.inspector.MetadataRetriever | ||
| import androidx.media3.transformer.Composition |
There was a problem hiding this comment.
The class androidx.media3.inspector.MetadataRetriever does not exist in the AndroidX Media3 library, which will cause a compilation failure. We should use the platform's android.media.MediaMetadataRetriever instead. Additionally, we need to import Dispatchers and withContext to run the retrieval on a background thread.
| import androidx.media3.common.util.UnstableApi | |
| import androidx.media3.inspector.MetadataRetriever | |
| import androidx.media3.transformer.Composition | |
| import androidx.media3.common.util.UnstableApi | |
| import android.media.MediaMetadataRetriever | |
| import kotlinx.coroutines.Dispatchers | |
| import kotlinx.coroutines.withContext | |
| import androidx.media3.transformer.Composition |
There was a problem hiding this comment.
The package exists in media3
| viewModelScope.launch { | ||
| val durations = coroutineScope { | ||
| videos.map { video -> | ||
| async { | ||
| val localUri = uriPairs.find { it.first == video.uri }?.second | ||
| if (localUri != null) { | ||
| try { | ||
| val mediaItem = MediaItem.fromUri(localUri) | ||
| val retriever = MetadataRetriever.Builder(context, mediaItem).build() | ||
| val durationUs = retriever.retrieveDurationUs().await() | ||
| retriever.close() | ||
| video.uri to durationUs | ||
| } catch (e: Exception) { | ||
| Log.e("VideoAnalysisVM", "Error retrieving duration for ${video.uri}", e) | ||
| null | ||
| } | ||
| } else { | ||
| null | ||
| } | ||
| } | ||
| }.awaitAll().filterNotNull().toMap() | ||
| } | ||
| _uiState.update { it.copy(durations = durations, isLoading = false) } |
There was a problem hiding this comment.
Since androidx.media3.inspector.MetadataRetriever is non-existent, we should use the platform's MediaMetadataRetriever to fetch the video duration. Furthermore, retrieving metadata is a blocking I/O operation that should be executed on Dispatchers.IO rather than the Main thread to prevent UI jank or ANRs.
viewModelScope.launch {
val durations = withContext(Dispatchers.IO) {
videos.map { video ->
async {
val localUri = uriPairs.find { it.first == video.uri }?.second
if (localUri != null) {
try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, localUri)
val durationMsStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
retriever.release()
val durationUs = (durationMsStr?.toLong() ?: 0L) * 1000L
video.uri to durationUs
} catch (e: Exception) {
Log.e("VideoAnalysisVM", "Error retrieving duration for ${video.uri}", e)
null
}
} else {
null
}
}
}.awaitAll().filterNotNull().toMap()
}
_uiState.update { it.copy(durations = durations, isLoading = false) }There was a problem hiding this comment.
Here is the link to the documentation: https://developer.android.com/media/media3/inspector
| import androidx.media3.ui.compose.PlayerSurface | ||
| import androidx.media3.ui.compose.buttons.PlayPauseButton | ||
| import androidx.media3.ui.compose.state.PlayPauseButtonState |
There was a problem hiding this comment.
The import of androidx.media3.ui.compose.buttons.PlayPauseButton conflicts with the custom PlayPauseButton composable function defined on line 136 of this file, causing a redeclaration compilation error. Removing this import resolves the conflict.
| import androidx.media3.ui.compose.PlayerSurface | |
| import androidx.media3.ui.compose.buttons.PlayPauseButton | |
| import androidx.media3.ui.compose.state.PlayPauseButtonState | |
| import androidx.media3.ui.compose.PlayerSurface | |
| import androidx.media3.ui.compose.state.PlayPauseButtonState |
| val endMs = it.endMs.coerceIn(0, totalDurationMs) | ||
| val startMs = it.startMs.coerceIn(0, endMs) |
There was a problem hiding this comment.
In Kotlin, Long.coerceIn requires both the minimum and maximum bounds to be of type Long. Passing 0 (which is an Int) causes a type mismatch compilation error. Using 0L resolves this issue.
| val endMs = it.endMs.coerceIn(0, totalDurationMs) | |
| val startMs = it.startMs.coerceIn(0, endMs) | |
| val endMs = it.endMs.coerceIn(0L, totalDurationMs) | |
| val startMs = it.startMs.coerceIn(0L, endMs) |
| val composition = Composition.Builder( | ||
| EditedMediaItemSequence.withAudioAndVideoFrom(editedMediaItems) | ||
| ).setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL).build() |
There was a problem hiding this comment.
EditedMediaItemSequence.withAudioAndVideoFrom is not a valid API in Media3. Use the standard constructor EditedMediaItemSequence(editedMediaItems) instead to avoid compilation errors.
| val composition = Composition.Builder( | |
| EditedMediaItemSequence.withAudioAndVideoFrom(editedMediaItems) | |
| ).setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL).build() | |
| val composition = Composition.Builder( | |
| EditedMediaItemSequence(editedMediaItems) | |
| ).setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL).build() |
There was a problem hiding this comment.
| class VideoAnalysisViewModel(application: Application) : AndroidViewModel(application) { | ||
| private val _uiState = MutableStateFlow(VideoAnalysisUiState()) | ||
| val uiState: StateFlow<VideoAnalysisUiState> = _uiState.asStateFlow() | ||
|
|
||
| fun initialize(jsonResponse: String, uriPairs: List<Pair<String, Uri>>) { | ||
| if (_uiState.value.videos.isNotEmpty()) return // Already initialized |
There was a problem hiding this comment.
Using _uiState.value.videos.isNotEmpty() as a guard prevents the screen from updating if the user goes back, edits again, and comes back with a new response (since the ViewModel might be reused and state is already populated). We can track the last processed jsonResponse in the ViewModel to safely skip initialization only when the input is identical, while correctly resetting state for new inputs.
| class VideoAnalysisViewModel(application: Application) : AndroidViewModel(application) { | |
| private val _uiState = MutableStateFlow(VideoAnalysisUiState()) | |
| val uiState: StateFlow<VideoAnalysisUiState> = _uiState.asStateFlow() | |
| fun initialize(jsonResponse: String, uriPairs: List<Pair<String, Uri>>) { | |
| if (_uiState.value.videos.isNotEmpty()) return // Already initialized | |
| class VideoAnalysisViewModel(application: Application) : AndroidViewModel(application) { | |
| private val _uiState = MutableStateFlow(VideoAnalysisUiState()) | |
| val uiState: StateFlow<VideoAnalysisUiState> = _uiState.asStateFlow() | |
| private var lastJsonResponse: String? = null | |
| fun initialize(jsonResponse: String, uriPairs: List<Pair<String, Uri>>) { | |
| if (jsonResponse == lastJsonResponse) return // Already initialized for this response | |
| lastJsonResponse = jsonResponse |
| .fillMaxWidth() | ||
| .height(56.dp) | ||
| .align(Alignment.BottomCenter), | ||
| enabled = uiState.selectedVideoUris.isNotEmpty() && !uiState.isLoading |
There was a problem hiding this comment.
We should require the user to enter a non-blank editing intent before enabling the 'Edit with Gemini' button. This prevents sending empty prompts to the AI model.
| enabled = uiState.selectedVideoUris.isNotEmpty() && !uiState.isLoading | |
| enabled = uiState.selectedVideoUris.isNotEmpty() && uiState.intentText.isNotBlank() && !uiState.isLoading |
| if (newUriPairs.isEmpty()) { | ||
| _uiState.update { | ||
| it.copy( | ||
| isLoading = false, | ||
| errorMessage = "No files were uploaded successfully. Cannot proceed." | ||
| ) | ||
| } | ||
| return@launch | ||
| } |
There was a problem hiding this comment.
If some uploads fail but others succeed, the app currently proceeds to call Gemini with a partial list of videos. This can lead to unexpected behavior or broken states. It is safer to abort the process and show an error if any of the selected videos fail to upload.
| if (newUriPairs.isEmpty()) { | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| errorMessage = "No files were uploaded successfully. Cannot proceed." | |
| ) | |
| } | |
| return@launch | |
| } | |
| if (newUriPairs.size < currentState.selectedVideoUris.size) { | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| errorMessage = "One or more videos failed to upload. Cannot proceed." | |
| ) | |
| } | |
| return@launch | |
| } |
An Android application demonstrating automated video highlight generation and intelligent cinematic editing suggestion using Vertex AI for Firebase and Firebase Cloud Storage.
Users can select multiple videos, specify editing goals in natural language (e.g., "create a fast-paced 15-second action reel"), upload them to Cloud Storage, and let Gemini analyze the content to recommend and preview video trims and edits.