From fd6cdd78f3f1476b5d01948ded976417db2a2719 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Thu, 6 Nov 2025 15:36:29 -0500 Subject: [PATCH] Add mpv profile --- .../wholphin/ui/playback/PlaybackViewModel.kt | 234 ++++++++++-------- .../wholphin/util/mpv/MpvDeviceProfile.kt | 79 ++++++ .../wholphin/util/mpv/MpvPlayer.kt | 9 +- .../util/profile/DeviceProfileUtils.kt | 4 +- 4 files changed, 220 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvDeviceProfile.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 12368d14..a8670100 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -55,6 +55,7 @@ import com.github.damontecres.wholphin.util.TrackSupport import com.github.damontecres.wholphin.util.checkForSupport import com.github.damontecres.wholphin.util.formatDateTime import com.github.damontecres.wholphin.util.mpv.MpvPlayer +import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile import com.github.damontecres.wholphin.util.seasonEpisodePadded import com.github.damontecres.wholphin.util.subtitleMimeTypes import com.github.damontecres.wholphin.util.supportItemKinds @@ -434,50 +435,50 @@ class PlaybackViewModel Timber.v("changeStreams direct play") val source = currentPlayback.mediaSourceInfo - val externalSubtitleCount = source.externalSubtitlesCount val externalSubtitle = source.findExternalSubtitle(subtitleIndex) - val result = - withContext(Dispatchers.Main) { - applyTrackSelections( - player, - playerBackend, - true, - audioIndex, - subtitleIndex, - source.videoStreamCount, - source.audioStreamCount, - externalSubtitleCount, - externalSubtitle != null, - ) - } - if (result.bothSelected) { - // TODO lots of duplicate code in this block - Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex") - val itemPlayback = - currentItemPlayback.copy( - sourceId = source.id?.toUUIDOrNull(), - audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, - subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED, - ) - if (userInitiated) { - viewModelScope.launchIO { - Timber.v("Saving user initiated item playback: %s", itemPlayback) - val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback) - withContext(Dispatchers.Main) { - this@PlaybackViewModel.currentItemPlayback.value = updated + if (externalSubtitle == null) { + val result = + withContext(Dispatchers.Main) { + applyTrackSelections( + player, + playerBackend, + true, + audioIndex, + subtitleIndex, + source, + ) + } + if (result.bothSelected) { + // TODO lots of duplicate code in this block + Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex") + val itemPlayback = + currentItemPlayback.copy( + sourceId = source.id?.toUUIDOrNull(), + audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, + subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED, + ) + if (userInitiated) { + viewModelScope.launchIO { + Timber.v("Saving user initiated item playback: %s", itemPlayback) + val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback) + withContext(Dispatchers.Main) { + this@PlaybackViewModel.currentItemPlayback.value = updated + } } } - } - withContext(Dispatchers.Main) { - this@PlaybackViewModel.currentPlayback.value = - currentPlayback.copy( - tracks = checkForSupport(player.currentTracks), - ) - this@PlaybackViewModel.currentItemPlayback.value = itemPlayback - } + withContext(Dispatchers.Main) { + this@PlaybackViewModel.currentPlayback.value = + currentPlayback.copy( + tracks = checkForSupport(player.currentTracks), + ) + this@PlaybackViewModel.currentItemPlayback.value = itemPlayback + } - return@withContext + return@withContext + } + } else { + Timber.v("changeStreams direct play, external subtitle was requested") } } @@ -509,7 +510,7 @@ class PlaybackViewModel if (playerBackend == PlayerBackend.EXO_PLAYER) { deviceProfile } else { - null + mpvDeviceProfile }, maxAudioChannels = null, audioStreamIndex = audioIndex, @@ -581,7 +582,7 @@ class PlaybackViewModel } } - Timber.v("externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle") + Timber.v("subtitleIndex=$subtitleIndex, externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle") val mediaItem = MediaItem @@ -648,18 +649,18 @@ class PlaybackViewModel override fun onTracksChanged(tracks: Tracks) { Timber.v("onTracksChanged: $tracks") if (tracks.groups.isNotEmpty()) { - applyTrackSelections( - player, - playerBackend, - source.supportsDirectPlay, - audioIndex, - subtitleIndex, - source.videoStreamCount, - source.audioStreamCount, - externalSubtitleCount, - externalSubtitle != null, - ) - player.removeListener(this) + val result = + applyTrackSelections( + player, + playerBackend, + source.supportsDirectPlay, + audioIndex, + subtitleIndex, + source, + ) + if (result.bothSelected) { + player.removeListener(this) + } } } } @@ -1152,6 +1153,14 @@ val MediaSourceInfo.externalSubtitlesCount: Int mediaStreams ?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0 +/** + * Returns the number of embedded subtitle streams there are + */ +val MediaSourceInfo.embeddedSubtitleCount: Int + get() = + mediaStreams + ?.count { it.type == MediaStreamType.SUBTITLE && !it.isExternal } ?: 0 + /** * Returns the number of video streams there are */ @@ -1193,58 +1202,67 @@ private fun applyTrackSelections( supportsDirectPlay: Boolean, audioIndex: Int?, subtitleIndex: Int?, - videoStreamCount: Int, - audioStreamCount: Int, - externalSubtitleCount: Int, - subtitleIsExternal: Boolean, + source: MediaSourceInfo, ): TrackSelectionResult { - val subtitleSelected = - if (subtitleIndex != null && subtitleIndex >= 0 && (subtitleIsExternal || supportsDirectPlay)) { - val chosenTrack = - if (subtitleIsExternal) { - player.currentTracks.groups.firstOrNull { group -> - group.type == C.TRACK_TYPE_TEXT && group.isSupported && - (0.. - group.type == C.TRACK_TYPE_TEXT && group.isSupported && - (0..= 0) { + val subtitleIsExternal = source.findExternalSubtitle(subtitleIndex) != null + if (subtitleIsExternal || supportsDirectPlay) { + val chosenTrack = + if (subtitleIsExternal && playerBackend == PlayerBackend.EXO_PLAYER) { + player.currentTracks.groups.firstOrNull { group -> + group.type == C.TRACK_TYPE_TEXT && group.isSupported && + (0.. + group.type == C.TRACK_TYPE_TEXT && group.isSupported && + (0.. @@ -1301,7 +1321,9 @@ private fun calculateIndexToFind( playerBackend: PlayerBackend, videoStreamCount: Int, audioStreamCount: Int, + embeddedSubtitleCount: Int, externalSubtitleCount: Int, + subtitleIsExternal: Boolean, ): Int = when (playerBackend) { PlayerBackend.EXO_PLAYER, @@ -1315,7 +1337,13 @@ private fun calculateIndexToFind( when (type) { MediaStreamType.VIDEO -> serverIndex - externalSubtitleCount + 1 MediaStreamType.AUDIO -> serverIndex - externalSubtitleCount - videoStreamCount + 1 - MediaStreamType.SUBTITLE -> serverIndex - externalSubtitleCount - videoStreamCount - audioStreamCount + 1 + MediaStreamType.SUBTITLE -> { + if (subtitleIsExternal) { + serverIndex + embeddedSubtitleCount + 1 + } else { + serverIndex - externalSubtitleCount - videoStreamCount - audioStreamCount + 1 + } + } else -> throw UnsupportedOperationException("Cannot calculate index for $type") } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvDeviceProfile.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvDeviceProfile.kt new file mode 100644 index 00000000..1bd04e93 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvDeviceProfile.kt @@ -0,0 +1,79 @@ +package com.github.damontecres.wholphin.util.mpv + +import com.github.damontecres.wholphin.util.profile.Codec +import com.github.damontecres.wholphin.util.profile.subtitleProfile +import com.github.damontecres.wholphin.util.profile.supportedAudioCodecs +import org.jellyfin.sdk.model.api.DlnaProfileType +import org.jellyfin.sdk.model.api.EncodingContext +import org.jellyfin.sdk.model.api.MediaStreamProtocol +import org.jellyfin.sdk.model.deviceprofile.buildDeviceProfile + +val mpvDeviceProfile = + buildDeviceProfile { + name = "mpv" + + // TODO +// maxStaticBitrate = maxBitrate +// maxStreamingBitrate = maxBitrate + transcodingProfile { + type = DlnaProfileType.VIDEO + context = EncodingContext.STREAMING + + container = Codec.Container.TS + protocol = MediaStreamProtocol.HLS + + videoCodec(Codec.Video.HEVC) + videoCodec(Codec.Video.H264) + + audioCodec(*supportedAudioCodecs) + + copyTimestamps = false + enableSubtitlesInManifest = true + } + directPlayProfile { + type = DlnaProfileType.VIDEO + + container( + Codec.Container.ASF, + Codec.Container.AVI, + Codec.Container.HLS, + Codec.Container.M4V, + Codec.Container.MPEG, + Codec.Container.MPEGTS, + Codec.Container.MKV, + Codec.Container.MOV, + Codec.Container.MP4, + Codec.Container.OGM, + Codec.Container.OGV, + Codec.Container.TS, + Codec.Container.VOB, + Codec.Container.WEBM, + Codec.Container.WMV, + Codec.Container.XVID, + ) + + videoCodec( + Codec.Video.AV1, + Codec.Video.H264, + Codec.Video.HEVC, + Codec.Video.MPEG, + Codec.Video.MPEG2VIDEO, + Codec.Video.VP8, + Codec.Video.VP9, + ) + + audioCodec(*supportedAudioCodecs) + } + + subtitleProfile(Codec.Subtitle.VTT, embedded = true, hls = true, external = true) + subtitleProfile(Codec.Subtitle.WEBVTT, embedded = true, hls = true, external = true) + subtitleProfile(Codec.Subtitle.SRT, embedded = true, external = true) + subtitleProfile(Codec.Subtitle.SUBRIP, embedded = true, external = true) + subtitleProfile(Codec.Subtitle.TTML, embedded = true, external = true) + subtitleProfile(Codec.Subtitle.DVBSUB, embedded = true, encode = true) + subtitleProfile(Codec.Subtitle.IDX, embedded = true, encode = true) + subtitleProfile(Codec.Subtitle.PGS, embedded = true, encode = true) + subtitleProfile(Codec.Subtitle.PGSSUB, embedded = true, encode = true) + subtitleProfile(Codec.Subtitle.ASS, encode = true, embedded = true, external = true) + subtitleProfile(Codec.Subtitle.SSA, encode = true, embedded = true, external = true) + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt index 45c976fc..f726e986 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt @@ -576,6 +576,11 @@ class MpvPlayer( Timber.d("Seeking to $startPositionMs") seekTo(startPositionMs) } + mediaItem!!.localConfiguration?.subtitleConfigurations?.forEach { + val url = it.uri.toString() + Timber.v("Adding external subtitle track $url") + MPVLib.command(arrayOf("sub-add", url, "auto")) + } getTracks().let { notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) } } @@ -675,7 +680,9 @@ class MpvPlayer( .setCodecs(codec) .setSampleMimeType(mimeType) .setLanguage(lang) - .setLabel(listOfNotNull(title, codecDescription).joinToString(",")) + // TODO title contains apikey for external subtitles + // .setLabel(listOfNotNull(title, codecDescription).joinToString(",")) + .setLabel(codecDescription) .setSelectionFlags(flags) channelCount?.let(builder::setChannelCount) val format = builder.build() diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt index fc7fcd11..9ab8e87b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt @@ -22,7 +22,7 @@ private val downmixSupportedAudioCodecs = Codec.Audio.MP3, ) -private val supportedAudioCodecs = +val supportedAudioCodecs = arrayOf( Codec.Audio.AAC, Codec.Audio.AAC_LATM, @@ -495,7 +495,7 @@ fun createDeviceProfile( } // Little helper function to more easily define subtitle profiles -private fun DeviceProfileBuilder.subtitleProfile( +fun DeviceProfileBuilder.subtitleProfile( format: String, embedded: Boolean = false, external: Boolean = false,