Skip to content

Video clipper #115

Open
droid-girl wants to merge 4 commits into
android:mainfrom
droid-girl:video-clipper
Open

Video clipper #115
droid-girl wants to merge 4 commits into
android:mainfrom
droid-girl:video-clipper

Conversation

@droid-girl

Copy link
Copy Markdown

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +13
import androidx.media3.common.util.UnstableApi
import androidx.media3.inspector.MetadataRetriever
import androidx.media3.transformer.Composition

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package exists in media3

Comment on lines +50 to +72
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) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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) }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the link to the documentation: https://developer.android.com/media/media3/inspector

Comment on lines +26 to +28
import androidx.media3.ui.compose.PlayerSurface
import androidx.media3.ui.compose.buttons.PlayPauseButton
import androidx.media3.ui.compose.state.PlayPauseButtonState

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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

Comment on lines +90 to +91
val endMs = it.endMs.coerceIn(0, totalDurationMs)
val startMs = it.startMs.coerceIn(0, endMs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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)

Comment on lines +105 to +107
val composition = Composition.Builder(
EditedMediaItemSequence.withAudioAndVideoFrom(editedMediaItems)
).setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL).build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

EditedMediaItemSequence.withAudioAndVideoFrom is not a valid API in Media3. Use the standard constructor EditedMediaItemSequence(editedMediaItems) instead to avoid compilation errors.

Suggested change
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()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +38 to +43
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
enabled = uiState.selectedVideoUris.isNotEmpty() && !uiState.isLoading
enabled = uiState.selectedVideoUris.isNotEmpty() && uiState.intentText.isNotBlank() && !uiState.isLoading

Comment on lines +82 to +90
if (newUriPairs.isEmpty()) {
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "No files were uploaded successfully. Cannot proceed."
)
}
return@launch
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant