Add mpv profile

This commit is contained in:
Damontecres 2025-11-06 15:36:29 -05:00
parent 0597e93178
commit fd6cdd78f3
No known key found for this signature in database
4 changed files with 220 additions and 106 deletions

View file

@ -55,6 +55,7 @@ import com.github.damontecres.wholphin.util.TrackSupport
import com.github.damontecres.wholphin.util.checkForSupport import com.github.damontecres.wholphin.util.checkForSupport
import com.github.damontecres.wholphin.util.formatDateTime import com.github.damontecres.wholphin.util.formatDateTime
import com.github.damontecres.wholphin.util.mpv.MpvPlayer 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.seasonEpisodePadded
import com.github.damontecres.wholphin.util.subtitleMimeTypes import com.github.damontecres.wholphin.util.subtitleMimeTypes
import com.github.damontecres.wholphin.util.supportItemKinds import com.github.damontecres.wholphin.util.supportItemKinds
@ -434,50 +435,50 @@ class PlaybackViewModel
Timber.v("changeStreams direct play") Timber.v("changeStreams direct play")
val source = currentPlayback.mediaSourceInfo val source = currentPlayback.mediaSourceInfo
val externalSubtitleCount = source.externalSubtitlesCount
val externalSubtitle = source.findExternalSubtitle(subtitleIndex) val externalSubtitle = source.findExternalSubtitle(subtitleIndex)
val result = if (externalSubtitle == null) {
withContext(Dispatchers.Main) { val result =
applyTrackSelections( withContext(Dispatchers.Main) {
player, applyTrackSelections(
playerBackend, player,
true, playerBackend,
audioIndex, true,
subtitleIndex, audioIndex,
source.videoStreamCount, subtitleIndex,
source.audioStreamCount, source,
externalSubtitleCount, )
externalSubtitle != null, }
) if (result.bothSelected) {
} // TODO lots of duplicate code in this block
if (result.bothSelected) { Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex")
// TODO lots of duplicate code in this block val itemPlayback =
Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex") currentItemPlayback.copy(
val itemPlayback = sourceId = source.id?.toUUIDOrNull(),
currentItemPlayback.copy( audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
sourceId = source.id?.toUUIDOrNull(), subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED,
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, )
subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED, if (userInitiated) {
) viewModelScope.launchIO {
if (userInitiated) { Timber.v("Saving user initiated item playback: %s", itemPlayback)
viewModelScope.launchIO { val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback)
Timber.v("Saving user initiated item playback: %s", itemPlayback) withContext(Dispatchers.Main) {
val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback) this@PlaybackViewModel.currentItemPlayback.value = updated
withContext(Dispatchers.Main) { }
this@PlaybackViewModel.currentItemPlayback.value = updated
} }
} }
} withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { this@PlaybackViewModel.currentPlayback.value =
this@PlaybackViewModel.currentPlayback.value = currentPlayback.copy(
currentPlayback.copy( tracks = checkForSupport(player.currentTracks),
tracks = checkForSupport(player.currentTracks), )
) this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
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) { if (playerBackend == PlayerBackend.EXO_PLAYER) {
deviceProfile deviceProfile
} else { } else {
null mpvDeviceProfile
}, },
maxAudioChannels = null, maxAudioChannels = null,
audioStreamIndex = audioIndex, audioStreamIndex = audioIndex,
@ -581,7 +582,7 @@ class PlaybackViewModel
} }
} }
Timber.v("externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle") Timber.v("subtitleIndex=$subtitleIndex, externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle")
val mediaItem = val mediaItem =
MediaItem MediaItem
@ -648,18 +649,18 @@ class PlaybackViewModel
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
Timber.v("onTracksChanged: $tracks") Timber.v("onTracksChanged: $tracks")
if (tracks.groups.isNotEmpty()) { if (tracks.groups.isNotEmpty()) {
applyTrackSelections( val result =
player, applyTrackSelections(
playerBackend, player,
source.supportsDirectPlay, playerBackend,
audioIndex, source.supportsDirectPlay,
subtitleIndex, audioIndex,
source.videoStreamCount, subtitleIndex,
source.audioStreamCount, source,
externalSubtitleCount, )
externalSubtitle != null, if (result.bothSelected) {
) player.removeListener(this)
player.removeListener(this) }
} }
} }
} }
@ -1152,6 +1153,14 @@ val MediaSourceInfo.externalSubtitlesCount: Int
mediaStreams mediaStreams
?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0 ?.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 * Returns the number of video streams there are
*/ */
@ -1193,58 +1202,67 @@ private fun applyTrackSelections(
supportsDirectPlay: Boolean, supportsDirectPlay: Boolean,
audioIndex: Int?, audioIndex: Int?,
subtitleIndex: Int?, subtitleIndex: Int?,
videoStreamCount: Int, source: MediaSourceInfo,
audioStreamCount: Int,
externalSubtitleCount: Int,
subtitleIsExternal: Boolean,
): TrackSelectionResult { ): TrackSelectionResult {
val subtitleSelected = val videoStreamCount = source.videoStreamCount
if (subtitleIndex != null && subtitleIndex >= 0 && (subtitleIsExternal || supportsDirectPlay)) { val audioStreamCount = source.audioStreamCount
val chosenTrack = val embeddedSubtitleCount = source.embeddedSubtitleCount
if (subtitleIsExternal) { val externalSubtitleCount = source.externalSubtitlesCount
player.currentTracks.groups.firstOrNull { group ->
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
(0..<group.mediaTrackGroup.length)
.mapNotNull {
group.getTrackFormat(it).id
}.any { it.endsWith("e:$subtitleIndex") }
}
} else {
// TODO this might be incorrect
val indexToFind =
calculateIndexToFind(
subtitleIndex,
MediaStreamType.SUBTITLE,
playerBackend,
videoStreamCount,
audioStreamCount,
externalSubtitleCount,
)
Timber.v("Chosen subtitle ($subtitleIndex/$indexToFind) track")
// subtitleIndex - externalSubtitleCount + 1
player.currentTracks.groups.firstOrNull { group ->
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
(0..<group.mediaTrackGroup.length)
.map {
group.getTrackFormat(it).idAsInt
}.contains(indexToFind)
}
}
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack") val subtitleSelected =
chosenTrack?.let { if (subtitleIndex != null && subtitleIndex >= 0) {
player.trackSelectionParameters = val subtitleIsExternal = source.findExternalSubtitle(subtitleIndex) != null
player.trackSelectionParameters if (subtitleIsExternal || supportsDirectPlay) {
.buildUpon() val chosenTrack =
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false) if (subtitleIsExternal && playerBackend == PlayerBackend.EXO_PLAYER) {
.setOverrideForType( player.currentTracks.groups.firstOrNull { group ->
TrackSelectionOverride( group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
chosenTrack.mediaTrackGroup, (0..<group.mediaTrackGroup.length)
0, .mapNotNull {
), group.getTrackFormat(it).id
).build() }.any { it.endsWith("e:$subtitleIndex") }
}
} else {
val indexToFind =
calculateIndexToFind(
subtitleIndex,
MediaStreamType.SUBTITLE,
playerBackend,
videoStreamCount,
audioStreamCount,
embeddedSubtitleCount,
externalSubtitleCount,
subtitleIsExternal,
)
Timber.v("Chosen subtitle ($subtitleIndex/$indexToFind) track")
// subtitleIndex - externalSubtitleCount + 1
val tracks = player.currentTracks.groups
tracks.firstOrNull { group ->
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
(0..<group.mediaTrackGroup.length)
.map {
group.getTrackFormat(it).idAsInt
}.contains(indexToFind)
}
}
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack")
chosenTrack?.let {
player.trackSelectionParameters =
player.trackSelectionParameters
.buildUpon()
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
.setOverrideForType(
TrackSelectionOverride(
chosenTrack.mediaTrackGroup,
0,
),
).build()
}
chosenTrack != null
} else {
false
} }
chosenTrack != null
} else { } else {
player.trackSelectionParameters = player.trackSelectionParameters =
player.trackSelectionParameters player.trackSelectionParameters
@ -1262,7 +1280,9 @@ private fun applyTrackSelections(
playerBackend, playerBackend,
videoStreamCount, videoStreamCount,
audioStreamCount, audioStreamCount,
embeddedSubtitleCount,
externalSubtitleCount, externalSubtitleCount,
false,
) )
val chosenTrack = val chosenTrack =
player.currentTracks.groups.firstOrNull { group -> player.currentTracks.groups.firstOrNull { group ->
@ -1301,7 +1321,9 @@ private fun calculateIndexToFind(
playerBackend: PlayerBackend, playerBackend: PlayerBackend,
videoStreamCount: Int, videoStreamCount: Int,
audioStreamCount: Int, audioStreamCount: Int,
embeddedSubtitleCount: Int,
externalSubtitleCount: Int, externalSubtitleCount: Int,
subtitleIsExternal: Boolean,
): Int = ): Int =
when (playerBackend) { when (playerBackend) {
PlayerBackend.EXO_PLAYER, PlayerBackend.EXO_PLAYER,
@ -1315,7 +1337,13 @@ private fun calculateIndexToFind(
when (type) { when (type) {
MediaStreamType.VIDEO -> serverIndex - externalSubtitleCount + 1 MediaStreamType.VIDEO -> serverIndex - externalSubtitleCount + 1
MediaStreamType.AUDIO -> serverIndex - externalSubtitleCount - videoStreamCount + 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") else -> throw UnsupportedOperationException("Cannot calculate index for $type")
} }
} }

View file

@ -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)
}

View file

@ -576,6 +576,11 @@ class MpvPlayer(
Timber.d("Seeking to $startPositionMs") Timber.d("Seeking to $startPositionMs")
seekTo(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 { getTracks().let {
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) } notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
} }
@ -675,7 +680,9 @@ class MpvPlayer(
.setCodecs(codec) .setCodecs(codec)
.setSampleMimeType(mimeType) .setSampleMimeType(mimeType)
.setLanguage(lang) .setLanguage(lang)
.setLabel(listOfNotNull(title, codecDescription).joinToString(",")) // TODO title contains apikey for external subtitles
// .setLabel(listOfNotNull(title, codecDescription).joinToString(","))
.setLabel(codecDescription)
.setSelectionFlags(flags) .setSelectionFlags(flags)
channelCount?.let(builder::setChannelCount) channelCount?.let(builder::setChannelCount)
val format = builder.build() val format = builder.build()

View file

@ -22,7 +22,7 @@ private val downmixSupportedAudioCodecs =
Codec.Audio.MP3, Codec.Audio.MP3,
) )
private val supportedAudioCodecs = val supportedAudioCodecs =
arrayOf( arrayOf(
Codec.Audio.AAC, Codec.Audio.AAC,
Codec.Audio.AAC_LATM, Codec.Audio.AAC_LATM,
@ -495,7 +495,7 @@ fun createDeviceProfile(
} }
// Little helper function to more easily define subtitle profiles // Little helper function to more easily define subtitle profiles
private fun DeviceProfileBuilder.subtitleProfile( fun DeviceProfileBuilder.subtitleProfile(
format: String, format: String,
embedded: Boolean = false, embedded: Boolean = false,
external: Boolean = false, external: Boolean = false,