feat: unify audio and video attachment playback (#1188)

This commit is contained in:
M57 authored and GitHub committed 2026-09-18 20:20:34 +03:00
1 parent 89cd598c25
commit abce60ad7a
23 files changed
+2640 -496

No files matched your search

@@ -1,5 +1,7 @@
package com.m57.hermescontrol.data.model
import com.m57.hermescontrol.ui.chat.MediaKind
import com.m57.hermescontrol.ui.chat.classifyMedia
import kotlinx.serialization.Serializable
/**
@@ -25,22 +27,23 @@ data class Attachment(
val gatewayUrl: String? = null,
val source: AttachmentSource = AttachmentSource.LOCAL,
) {
val mediaKind: MediaKind
get() = classifyMedia(mimeType = mimeType, name = name, uri = gatewayUrl ?: uri)
val isImage: Boolean
get() = mimeType.startsWith("image/")
get() = mediaKind == MediaKind.IMAGE
val isGif: Boolean
get() =
mimeType.equals("image/gif", ignoreCase = true) ||
fileExtension == "gif" ||
uri.contains(".gif", ignoreCase = true) ||
uri.startsWith("data:image/gif", ignoreCase = true)
val isVideo: Boolean
get() =
mimeType.startsWith("video/") ||
fileExtension in listOf("mp4", "webm", "mkv", "mov", "avi", "3gp") ||
uri.contains(".mp4", ignoreCase = true) ||
uri.contains(".webm", ignoreCase = true)
get() = mediaKind == MediaKind.VIDEO
val isAudio: Boolean
get() = mediaKind == MediaKind.AUDIO
val isGateway: Boolean
get() = source == AttachmentSource.GATEWAY
@@ -277,6 +277,7 @@ object GatewayFileClient {
coroutineContext.ensureActive()
val read = input.read(buffer)
if (read < 0) break
coroutineContext.ensureActive()
output.write(buffer, 0, read)
}
}
@@ -0,0 +1,73 @@
package com.m57.hermescontrol.data.remote
import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import com.m57.hermescontrol.data.local.AuthManager
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
@OptIn(UnstableApi::class)
object MediaDataSourceProvider {
class SameOriginAuthInterceptor(
private val baseUrlProvider: () -> String = { runCatching { AuthManager.baseUrl() }.getOrDefault("") },
private val tokenProvider: () -> String? = { runCatching { AuthManager.getToken() }.getOrNull() },
private val isGatedModeProvider: () -> Boolean = {
runCatching { AuthManager.isGatedMode() }.getOrDefault(
false,
)
},
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// In gated mode (basic auth), the dashboard authenticates via session cookies;
// stamping an Authorization: Bearer header causes 401s (issue #470).
if (isGatedModeProvider()) {
return chain.proceed(request)
}
val url = request.url
val base = baseUrlProvider().toHttpUrlOrNull()
val isSameOrigin =
base != null &&
url.scheme.equals(base.scheme, ignoreCase = true) &&
url.host.equals(base.host, ignoreCase = true) &&
url.port == base.port
if (isSameOrigin) {
val token = tokenProvider()
if (!token.isNullOrBlank() && request.header("Authorization") == null) {
return chain.proceed(
request
.newBuilder()
.header("Authorization", "Bearer $token")
.build(),
)
}
}
return chain.proceed(request)
}
}
@Volatile
internal var testClient: OkHttpClient? = null
val mediaOkHttpClient: OkHttpClient
get() = testClient ?: defaultClient
private val defaultClient: OkHttpClient by lazy {
OkHttpProvider.base
.newBuilder()
.addInterceptor(SameOriginAuthInterceptor())
.build()
}
fun createDataSourceFactory(context: Context): DataSource.Factory {
val httpDataSourceFactory = OkHttpDataSource.Factory(mediaOkHttpClient)
return DefaultDataSource.Factory(context, httpDataSourceFactory)
}
}