diff --git a/playback/core/src/main/kotlin/mediastream/MediaStream.kt b/playback/core/src/main/kotlin/mediastream/MediaStream.kt index 4e21e02a30..1e43617526 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStream.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStream.kt @@ -52,4 +52,8 @@ data class MediaStreamAudioTrack( val sampleRate: Int, ) : MediaStreamTrack -// TODO: Add Video/Subtitle tracks +data class MediaStreamVideoTrack( + override val codec: String, +) : MediaStreamTrack + +// TODO: Add subtitle track diff --git a/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt b/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt index 2ebcfa7035..ea7f25b388 100644 --- a/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt +++ b/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt @@ -29,7 +29,7 @@ import org.jellyfin.playback.core.support.PlaySupportReport import org.jellyfin.playback.core.ui.PlayerSubtitleView import org.jellyfin.playback.core.ui.PlayerSurfaceView import org.jellyfin.playback.exoplayer.support.getPlaySupportReport -import org.jellyfin.playback.exoplayer.support.toFormat +import org.jellyfin.playback.exoplayer.support.toFormats import timber.log.Timber import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO @@ -111,7 +111,7 @@ class ExoPlayerBackend( override fun supportsStream( stream: MediaStream - ): PlaySupportReport = exoPlayer.getPlaySupportReport(stream.toFormat()) + ): PlaySupportReport = exoPlayer.getPlaySupportReport(stream.toFormats()) override fun setSurfaceView(surfaceView: PlayerSurfaceView?) { exoPlayer.setVideoSurfaceView(surfaceView?.surface) diff --git a/playback/exoplayer/src/main/kotlin/mapping/container.kt b/playback/exoplayer/src/main/kotlin/mapping/container.kt index a976a1b42d..3e30858a12 100644 --- a/playback/exoplayer/src/main/kotlin/mapping/container.kt +++ b/playback/exoplayer/src/main/kotlin/mapping/container.kt @@ -23,6 +23,7 @@ val ffmpegContainerMimeTypes = mapOf( "flac" to MimeTypes.AUDIO_FLAC, "flv" to MimeTypes.VIDEO_FLV, "matroska" to MimeTypes.APPLICATION_MATROSKA, + "mkv" to MimeTypes.APPLICATION_MATROSKA, "mjpeg" to MimeTypes.VIDEO_MJPEG, "mpeg" to MimeTypes.AUDIO_MPEG, "ogg" to MimeTypes.AUDIO_OGG, diff --git a/playback/exoplayer/src/main/kotlin/mapping/video.kt b/playback/exoplayer/src/main/kotlin/mapping/video.kt index 5f2df00113..3d49cbca6c 100644 --- a/playback/exoplayer/src/main/kotlin/mapping/video.kt +++ b/playback/exoplayer/src/main/kotlin/mapping/video.kt @@ -11,6 +11,23 @@ fun getFfmpegVideoMimeType(codec: String): String { ?: codec } -val ffmpegVideoMimeTypes = mapOf( - // TODO: Add map +@OptIn(UnstableApi::class) +val ffmpegVideoMimeTypes = mapOf( + "mp4" to MimeTypes.VIDEO_MP4, + "mkv" to MimeTypes.VIDEO_MATROSKA, + "webm" to MimeTypes.VIDEO_WEBM, + "h263" to MimeTypes.VIDEO_H263, + "h254" to MimeTypes.VIDEO_H264, + "h265" to MimeTypes.VIDEO_H265, + "vp8" to MimeTypes.VIDEO_VP8, + "vp9" to MimeTypes.VIDEO_VP9, + "av1" to MimeTypes.VIDEO_AV1, + "mpeg" to MimeTypes.VIDEO_MPEG, + "mp2" to MimeTypes.VIDEO_MPEG2, + "vc1" to MimeTypes.VIDEO_VC1, + "flv" to MimeTypes.VIDEO_FLV, + "ogv" to MimeTypes.VIDEO_OGG, + "avi" to MimeTypes.VIDEO_AVI, + "mjpeg" to MimeTypes.VIDEO_MJPEG, + "rawvideo" to MimeTypes.VIDEO_RAW, ) diff --git a/playback/exoplayer/src/main/kotlin/support/ExoPlayerPlaySupportReport.kt b/playback/exoplayer/src/main/kotlin/support/ExoPlayerPlaySupportReport.kt index 41f3a1ef04..a2e318fcbc 100644 --- a/playback/exoplayer/src/main/kotlin/support/ExoPlayerPlaySupportReport.kt +++ b/playback/exoplayer/src/main/kotlin/support/ExoPlayerPlaySupportReport.kt @@ -46,6 +46,12 @@ data class ExoPlayerPlaySupportReport( fun ExoPlayer.getPlaySupportReport(format: Format): ExoPlayerPlaySupportReport = ExoPlayerPlaySupportReport.fromFlags(supportsFormat(format)) +fun ExoPlayer.getPlaySupportReport(formats: Collection): ExoPlayerPlaySupportReport = formats + .map { format -> supportsFormat(format) } + .reduce { acc, i -> acc and i } + .let { flags -> ExoPlayerPlaySupportReport.fromFlags(flags) } + + @OptIn(UnstableApi::class) fun ExoPlayer.supportsFormat(format: Format): Int { var capabilities = 0 diff --git a/playback/exoplayer/src/main/kotlin/support/mediaStreamToFormat.kt b/playback/exoplayer/src/main/kotlin/support/mediaStreamToFormat.kt index c9550dd071..9d67de0a2f 100644 --- a/playback/exoplayer/src/main/kotlin/support/mediaStreamToFormat.kt +++ b/playback/exoplayer/src/main/kotlin/support/mediaStreamToFormat.kt @@ -5,19 +5,35 @@ import androidx.media3.common.Format import androidx.media3.common.util.UnstableApi import org.jellyfin.playback.core.mediastream.MediaStream import org.jellyfin.playback.core.mediastream.MediaStreamAudioTrack +import org.jellyfin.playback.core.mediastream.MediaStreamVideoTrack import org.jellyfin.playback.exoplayer.mapping.getFfmpegAudioMimeType import org.jellyfin.playback.exoplayer.mapping.getFfmpegContainerMimeType +import org.jellyfin.playback.exoplayer.mapping.getFfmpegVideoMimeType @OptIn(UnstableApi::class) -fun MediaStream.toFormat() = Format.Builder().also { f -> - f.setId(identifier) - f.setContainerMimeType(getFfmpegContainerMimeType(container.format)) +fun toFormat(stream: MediaStream, track: MediaStreamAudioTrack) = Format.Builder().also { f -> + f.setId(stream.identifier) + f.setContainerMimeType(getFfmpegContainerMimeType(stream.container.format)) - val audioTrack = tracks.filterIsInstance().firstOrNull() - if (audioTrack != null) { - f.setSampleMimeType(getFfmpegAudioMimeType(audioTrack.codec)) - f.setChannelCount(audioTrack.channels) - f.setAverageBitrate(audioTrack.bitrate) - f.setSampleRate(audioTrack.sampleRate) - } + f.setCodecs(track.codec) + f.setSampleMimeType(getFfmpegAudioMimeType(track.codec)) + f.setChannelCount(track.channels) + f.setAverageBitrate(track.bitrate) + f.setSampleRate(track.sampleRate) }.build() + +@OptIn(UnstableApi::class) +fun toFormat(stream: MediaStream, track: MediaStreamVideoTrack) = Format.Builder().also { f -> + f.setId(stream.identifier) + f.setContainerMimeType(getFfmpegContainerMimeType(stream.container.format)) + + f.setCodecs(track.codec) + f.setSampleMimeType(getFfmpegVideoMimeType(track.codec)) +}.build() + +fun MediaStream.toFormats() = tracks.map { track -> + when (track) { + is MediaStreamAudioTrack -> toFormat(stream = this, track) + is MediaStreamVideoTrack -> toFormat(stream = this, track) + } +} diff --git a/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt b/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt index 5bd93d290a..1afb40d5e9 100644 --- a/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt +++ b/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt @@ -24,9 +24,9 @@ fun jellyfinPlugin( responseProfiles = emptyList(), subtitleProfiles = emptyList(), supportedMediaTypes = "", - // Add at least one transcoding profile so the server returns a value - // for "SupportsTranscoding" based on the user policy - // We don't actually use this profile in the client + // Add at least one transcoding profile for both audio an video so the server returns a + // value for "SupportsTranscoding" based on the user policy. We don't actually use this + // profile in the client transcodingProfiles = listOf( TranscodingProfile( type = DlnaProfileType.AUDIO, @@ -36,6 +36,15 @@ fun jellyfinPlugin( audioCodec = "mp3", videoCodec = "", conditions = emptyList() + ), + TranscodingProfile( + type = DlnaProfileType.VIDEO, + context = EncodingContext.STREAMING, + protocol = "hls", + container = "ts", + audioCodec = "aac", + videoCodec = "h264", + conditions = emptyList() ) ), xmlRootAttributes = emptyList(), diff --git a/playback/jellyfin/src/main/kotlin/mediastream/VideoMediaStreamResolver.kt b/playback/jellyfin/src/main/kotlin/mediastream/VideoMediaStreamResolver.kt index 2a28d53712..612de4a961 100644 --- a/playback/jellyfin/src/main/kotlin/mediastream/VideoMediaStreamResolver.kt +++ b/playback/jellyfin/src/main/kotlin/mediastream/VideoMediaStreamResolver.kt @@ -101,6 +101,8 @@ class VideoMediaStreamResolver( playSessionId = mediaInfo.playSessionId, tag = mediaInfo.mediaSource.eTag, segmentContainer = REMUX_SEGMENT_CONTAINER, + videoCodec = "h264", + audioCodec = "aac", ) ) } diff --git a/playback/jellyfin/src/main/kotlin/mediastream/tracks.kt b/playback/jellyfin/src/main/kotlin/mediastream/tracks.kt index e16bc5bbb4..9ea99fc810 100644 --- a/playback/jellyfin/src/main/kotlin/mediastream/tracks.kt +++ b/playback/jellyfin/src/main/kotlin/mediastream/tracks.kt @@ -2,6 +2,7 @@ package org.jellyfin.playback.jellyfin.mediastream import org.jellyfin.playback.core.mediastream.MediaStreamAudioTrack import org.jellyfin.playback.core.mediastream.MediaStreamContainer +import org.jellyfin.playback.core.mediastream.MediaStreamVideoTrack import org.jellyfin.sdk.model.api.MediaStream import org.jellyfin.sdk.model.api.MediaStreamType @@ -16,7 +17,7 @@ fun JellyfinStreamResolver.MediaInfo.getTracks() = fun MediaStream.getMediaStreamTrack() = when (type) { MediaStreamType.AUDIO -> getAudioTrack(this) - MediaStreamType.VIDEO -> getVideooTrack(this) + MediaStreamType.VIDEO -> getVideoTrack(this) MediaStreamType.SUBTITLE -> getSubtitleTrack(this) // Ignore other track types @@ -32,8 +33,9 @@ private fun getAudioTrack(stream: MediaStream) = MediaStreamAudioTrack( sampleRate = stream.sampleRate ?: 0, ) -// TODO Implement Video track type -private fun getVideooTrack(stream: MediaStream) = null +private fun getVideoTrack(stream: MediaStream) = MediaStreamVideoTrack( + codec = requireNotNull(stream.codec), +) // TODO Implement Subtitle track type private fun getSubtitleTrack(stream: MediaStream) = null