ui: send dropped .webm as video only when it has a video track (#7354)

* ui: send dropped .webm as video only when it has a video track

.webm is as commonly an audio-only container as a video one, so the
extension alone cannot tell whether there is a frame to embed. Read the
container to decide: files with a video track are sent as video, the rest
as files.

Only done for files attached without the user saying how to send them
(drag & drop, paste). An explicitly picked video is still sent as one, so
.webm is now listed in the video file picker too.

* plan: send dropped .webm as video only when it has a video track
This commit is contained in:
Narasimha-sc
2026-08-12 15:29:43 +01:00
committed by GitHub
parent 0a61b0ddea
commit abd5954675
7 changed files with 100 additions and 3 deletions
@@ -285,7 +285,22 @@ expect fun AttachmentSelection(
)
fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) {
val groups = uris.groupBy { isImage(it) || isVideoUri(it) }
// The extension is enough to classify every format except .webm, which is just as commonly an
// audio-only container as a video one. An audio-only file has no frame to embed and is sent as a file,
// but that can only be told from the content, so reading it is deferred to a background thread.
// Only done here, where files arrive without the user saying how to send them (drag & drop, paste) -
// an explicitly picked video is still sent as one.
if (uris.none { isWebmUri(it) }) {
attachFiles(uris, emptySet())
} else {
CoroutineScope(Dispatchers.IO).launch {
attachFiles(uris, uris.filter { isWebmUri(it) && hasVideoTrack(it) }.toSet())
}
}
}
private fun MutableState<ComposeState>.attachFiles(uris: List<URI>, webmVideos: Set<URI>) {
val groups = uris.groupBy { isImage(it) || (isVideoUri(it) && (!isWebmUri(it) || it in webmVideos)) }
val media = groups[true] ?: emptyList()
val files = groups[false] ?: emptyList()
if (media.isNotEmpty()) {
@@ -298,9 +313,12 @@ fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) {
private fun isVideoUri(uri: URI): Boolean {
val name = getFileName(uri)?.lowercase() ?: return false
return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") ||
name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv")
name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv") ||
name.endsWith(".webm")
}
private fun isWebmUri(uri: URI): Boolean = getFileName(uri)?.lowercase()?.endsWith(".webm") == true
fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
if (uri != null) {
val maxFileSize = value.maxFileSize
@@ -495,6 +495,9 @@ fun ciSenderProfile(ci: ChatItem, chatInfo: ChatInfo): LocalProfile? = when (val
expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration
// Whether the file really contains a video track. Reads container metadata only, without decoding a frame.
expect suspend fun hasVideoTrack(uri: URI): Boolean
fun showWrongUriAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.non_content_uri_alert_title),