From d41f992b62a0f70d24e01500fdc883ac3714ffa5 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Mon, 2 Feb 2026 21:03:26 +0100 Subject: [PATCH] Add media type API to set proper content type in audio attributes --- .../kotlin/mediastream/mediatype/MediaType.kt | 21 ++++++++++++ .../mediastream/mediatype/mediaTypeElement.kt | 20 +++++++++++ .../kotlin/queue/createBaseItemQueueEntry.kt | 10 ++++++ .../src/main/kotlin/AudioAttributeState.kt | 24 +++++++++++++ .../src/main/kotlin/ExoPlayerBackend.kt | 34 +++++++++++++------ 5 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 playback/core/src/main/kotlin/mediastream/mediatype/MediaType.kt create mode 100644 playback/core/src/main/kotlin/mediastream/mediatype/mediaTypeElement.kt create mode 100644 playback/media3/exoplayer/src/main/kotlin/AudioAttributeState.kt diff --git a/playback/core/src/main/kotlin/mediastream/mediatype/MediaType.kt b/playback/core/src/main/kotlin/mediastream/mediatype/MediaType.kt new file mode 100644 index 000000000..59fee7d7d --- /dev/null +++ b/playback/core/src/main/kotlin/mediastream/mediatype/MediaType.kt @@ -0,0 +1,21 @@ +package org.jellyfin.playback.core.mediastream.mediatype + +/** + * Definition of the type for a specific media. + */ +enum class MediaType { + /** + * Media type for primarily video content like movies, tv shows and live tv. + */ + Video, + + /** + * Media type for primarily audio content like music, podcasts and audio books. + */ + Audio, + + /** + * Unknown media type. + */ + Unknown, +} diff --git a/playback/core/src/main/kotlin/mediastream/mediatype/mediaTypeElement.kt b/playback/core/src/main/kotlin/mediastream/mediatype/mediaTypeElement.kt new file mode 100644 index 000000000..f623b963d --- /dev/null +++ b/playback/core/src/main/kotlin/mediastream/mediatype/mediaTypeElement.kt @@ -0,0 +1,20 @@ +package org.jellyfin.playback.core.mediastream.mediatype + +import org.jellyfin.playback.core.element.ElementKey +import org.jellyfin.playback.core.element.elementFlow +import org.jellyfin.playback.core.element.requiredElement +import org.jellyfin.playback.core.queue.QueueEntry + +private val mediaTypeKey = ElementKey("MediaType") + +/** + * Get or set the media type this [QueueEntry]. A supported backend will use this to + * set correct audio attributes for the media. + */ +var QueueEntry.mediaType by requiredElement(mediaTypeKey) { MediaType.Unknown } + +/** + * Get the flow of [mediaType]. + * @see mediaType + */ +val QueueEntry.mediaTypeFlow by elementFlow(mediaTypeKey) diff --git a/playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt b/playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt index cc9c70a05..b9aa7f6d0 100644 --- a/playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt +++ b/playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt @@ -1,5 +1,6 @@ package org.jellyfin.playback.jellyfin.queue +import org.jellyfin.playback.core.mediastream.mediatype.mediaType import org.jellyfin.playback.core.mediastream.normalizationGain import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.playback.core.queue.QueueEntryMetadata @@ -10,6 +11,8 @@ import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID +import org.jellyfin.playback.core.mediastream.mediatype.MediaType as PlayerMediaType +import org.jellyfin.sdk.model.api.MediaType as SdkMediaType /** * Create a [QueueEntry] from a [BaseItemDto]. @@ -46,6 +49,13 @@ fun createBaseItemQueueEntry(api: ApiClient, baseItem: BaseItemDto): QueueEntry ) entry.baseItem = baseItem entry.normalizationGain = baseItem.normalizationGain + entry.mediaType = when (baseItem.mediaType) { + SdkMediaType.VIDEO -> PlayerMediaType.Video + SdkMediaType.AUDIO -> PlayerMediaType.Audio + SdkMediaType.PHOTO, + SdkMediaType.BOOK, + SdkMediaType.UNKNOWN -> PlayerMediaType.Unknown + } return entry } diff --git a/playback/media3/exoplayer/src/main/kotlin/AudioAttributeState.kt b/playback/media3/exoplayer/src/main/kotlin/AudioAttributeState.kt new file mode 100644 index 000000000..5782c4e64 --- /dev/null +++ b/playback/media3/exoplayer/src/main/kotlin/AudioAttributeState.kt @@ -0,0 +1,24 @@ +package org.jellyfin.playback.media3.exoplayer + +import androidx.media3.common.AudioAttributes +import timber.log.Timber + +class AudioAttributeState { + private var _audioAttributes: AudioAttributes? = null + + fun updateAudioAttributes( + builder: AudioAttributes.Builder.() -> Unit, + onChange: (audioAttributes: AudioAttributes) -> Unit, + ) { + val newAudioAttributes = AudioAttributes.Builder().apply { + builder() + }.build() + + if (_audioAttributes != newAudioAttributes) { + Timber.d("Audio attributes changed") + + onChange(newAudioAttributes) + _audioAttributes = newAudioAttributes + } + } +} diff --git a/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt b/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt index 428827cea..f7686577d 100644 --- a/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt +++ b/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt @@ -5,7 +5,6 @@ import android.content.Context import android.view.ViewGroup import androidx.annotation.OptIn import androidx.core.content.getSystemService -import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException @@ -27,6 +26,8 @@ import org.jellyfin.playback.core.backend.BasePlayerBackend import org.jellyfin.playback.core.mediastream.MediaStream import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.mediastream.mediaStream +import org.jellyfin.playback.core.mediastream.mediatype.MediaType +import org.jellyfin.playback.core.mediastream.mediatype.mediaType import org.jellyfin.playback.core.mediastream.normalizationGain import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PositionInfo @@ -53,7 +54,9 @@ class ExoPlayerBackend( private var currentStream: PlayableMediaStream? = null private var subtitleView: SubtitleView? = null - private var audioPipeline = ExoPlayerAudioPipeline() + private val audioPipeline = ExoPlayerAudioPipeline() + private val audioAttributeState = AudioAttributeState() + private val exoPlayer by lazy { val dataSourceFactory = DefaultDataSource.Factory( context, @@ -95,9 +98,6 @@ class ExoPlayerBackend( }) }) .setMediaSourceFactory(mediaSourceFactory) - .setAudioAttributes(AudioAttributes.Builder().apply { - setUsage(C.USAGE_MEDIA) - }.build(), true) .setPauseAtEndOfMediaItems(true) .build() .also { player -> @@ -208,19 +208,33 @@ class ExoPlayerBackend( preparedItemIndex = exoPlayer.mediaItemCount - 1 } - // Determine how to play item - if (preparedItemIndex == exoPlayer.currentMediaItemIndex) return - - Timber.i("Playing ${item.mediaStream?.url}") - // Seek to prepared media item when (preparedItemIndex) { exoPlayer.currentMediaItemIndex - 1 -> exoPlayer.seekToPreviousMediaItem() exoPlayer.currentMediaItemIndex + 1 -> exoPlayer.seekToNextMediaItem() + exoPlayer.currentMediaItemIndex -> Unit else -> exoPlayer.seekTo(preparedItemIndex, 0) } + // Update audio attributes + val contentType = when (item.mediaType) { + MediaType.Video -> C.AUDIO_CONTENT_TYPE_MOVIE + MediaType.Audio -> C.AUDIO_CONTENT_TYPE_MUSIC + MediaType.Unknown -> C.AUDIO_CONTENT_TYPE_UNKNOWN + } + + audioAttributeState.updateAudioAttributes( + builder = { + setContentType(contentType) + setUsage(C.USAGE_MEDIA) + }, + onChange = { audioAttributes -> + exoPlayer.setAudioAttributes(audioAttributes, true) + } + ) + // Enjoy! + Timber.i("Playing ${item.mediaStream?.url}") exoPlayer.play() }