mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Update track selection logic for missing IDs
This commit is contained in:
parent
abf5ec8004
commit
73206c37d0
3 changed files with 200 additions and 146 deletions
|
|
@ -2,18 +2,17 @@ package com.github.damontecres.wholphin.ui.playback
|
|||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
object TrackSelectionUtils {
|
||||
@OptIn(UnstableApi::class)
|
||||
|
|
@ -26,8 +25,8 @@ object TrackSelectionUtils {
|
|||
subtitleIndex: Int?,
|
||||
source: MediaSourceInfo,
|
||||
): TrackSelectionResult {
|
||||
val embeddedSubtitleCount = source.embeddedSubtitleCount
|
||||
val externalSubtitleCount = source.externalSubtitlesCount
|
||||
// This function's implementation assumes that indexes for each MediaStream in the MediaSourceInfo
|
||||
// will be ordered as: external subtitles, video stream, audio stream, embedded subtitles
|
||||
|
||||
val paramsBuilder = trackSelectionParams.buildUpon()
|
||||
val groups = tracks.groups
|
||||
|
|
@ -35,69 +34,62 @@ object TrackSelectionUtils {
|
|||
val subtitleSelected =
|
||||
if (subtitleIndex != null && subtitleIndex >= 0) {
|
||||
val subtitleIsExternal = source.findExternalSubtitle(subtitleIndex) != null
|
||||
if (subtitleIsExternal || supportsDirectPlay) {
|
||||
val chosenTrack =
|
||||
if (subtitleIsExternal && playerBackend == PlayerBackend.EXO_PLAYER) {
|
||||
val chosenTrack =
|
||||
if (subtitleIsExternal) {
|
||||
// If external, only one external track should exist, so just find it
|
||||
val group =
|
||||
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") }
|
||||
.any {
|
||||
group.getTrackFormat(0).id?.contains("e:") == true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val actualEmbeddedCount =
|
||||
groups
|
||||
.filter { group ->
|
||||
group.type == C.TRACK_TYPE_TEXT &&
|
||||
(0..<group.mediaTrackGroup.length)
|
||||
.mapNotNull {
|
||||
group.getTrackFormat(it).id
|
||||
}.none { it.contains("e:") }
|
||||
}.size
|
||||
val indexToFind =
|
||||
calculateIndexToFind(
|
||||
subtitleIndex,
|
||||
MediaStreamType.SUBTITLE,
|
||||
playerBackend,
|
||||
embeddedSubtitleCount,
|
||||
externalSubtitleCount,
|
||||
subtitleIsExternal,
|
||||
actualEmbeddedCount,
|
||||
source,
|
||||
)
|
||||
Timber.v("Chosen subtitle ($subtitleIndex/$indexToFind) track")
|
||||
// subtitleIndex - externalSubtitleCount + 1
|
||||
groups.firstOrNull { group ->
|
||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
||||
(0..<group.mediaTrackGroup.length)
|
||||
.filter {
|
||||
if (subtitleIsExternal) {
|
||||
Timber.v(
|
||||
"Chosen external subtitle ($subtitleIndex/) track: %s",
|
||||
group,
|
||||
)
|
||||
group
|
||||
} else {
|
||||
// Not external
|
||||
// Find the first index of a embedded subtitle to use as an offset for desired subtitle index
|
||||
val firstEmbeddedSubtitleIndex =
|
||||
source.mediaStreams
|
||||
.orEmpty()
|
||||
.indexOfFirstOrNull {
|
||||
it.type == MediaStreamType.SUBTITLE &&
|
||||
!(it.deliveryMethod == SubtitleDeliveryMethod.EXTERNAL || it.isExternal)
|
||||
}
|
||||
if (firstEmbeddedSubtitleIndex != null) {
|
||||
val indexToFind = subtitleIndex - firstEmbeddedSubtitleIndex
|
||||
val subtitleTracks =
|
||||
groups.filter { group ->
|
||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
||||
(0..<group.mediaTrackGroup.length)
|
||||
.none {
|
||||
group.getTrackFormat(0).id?.contains("e:") == true
|
||||
} else {
|
||||
group.getTrackFormat(0).id?.contains("e:") == false
|
||||
}
|
||||
}.map {
|
||||
group.getTrackFormat(it).idAsInt
|
||||
}.contains(indexToFind)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack")
|
||||
chosenTrack?.let {
|
||||
paramsBuilder
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||
.setOverrideForType(
|
||||
TrackSelectionOverride(
|
||||
chosenTrack.mediaTrackGroup,
|
||||
0,
|
||||
),
|
||||
}
|
||||
Timber.v(
|
||||
"Chosen eembedded subtitle ($subtitleIndex/$indexToFind) track: %s",
|
||||
subtitleTracks.getOrNull(indexToFind),
|
||||
)
|
||||
subtitleTracks.getOrNull(indexToFind)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
chosenTrack != null
|
||||
} else {
|
||||
false
|
||||
chosenTrack?.let {
|
||||
paramsBuilder
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||
.setOverrideForType(
|
||||
TrackSelectionOverride(
|
||||
chosenTrack.mediaTrackGroup,
|
||||
0,
|
||||
),
|
||||
)
|
||||
}
|
||||
chosenTrack != null
|
||||
} else {
|
||||
paramsBuilder
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||
|
|
@ -106,26 +98,24 @@ object TrackSelectionUtils {
|
|||
}
|
||||
val audioSelected =
|
||||
if (audioIndex != null && supportsDirectPlay) {
|
||||
val indexToFind =
|
||||
calculateIndexToFind(
|
||||
audioIndex,
|
||||
MediaStreamType.AUDIO,
|
||||
playerBackend,
|
||||
embeddedSubtitleCount,
|
||||
externalSubtitleCount,
|
||||
false,
|
||||
null,
|
||||
source,
|
||||
)
|
||||
// Find the first index of an audio stream to use as an offset for desired index
|
||||
val firstAudioIndex =
|
||||
source.mediaStreams
|
||||
.orEmpty()
|
||||
.indexOfFirstOrNull { it.type == MediaStreamType.AUDIO }
|
||||
val chosenTrack =
|
||||
groups.firstOrNull { group ->
|
||||
group.type == C.TRACK_TYPE_AUDIO && group.isSupported &&
|
||||
(0..<group.mediaTrackGroup.length)
|
||||
.map {
|
||||
group.getTrackFormat(it).idAsInt
|
||||
}.contains(indexToFind)
|
||||
if (firstAudioIndex != null) {
|
||||
val indexToFind = audioIndex - firstAudioIndex
|
||||
val audioGroups =
|
||||
groups.filter { group -> group.type == C.TRACK_TYPE_AUDIO && group.isSupported }
|
||||
Timber.v(
|
||||
"Chosen audio ($audioIndex/$indexToFind) track: %s",
|
||||
audioGroups.getOrNull(indexToFind),
|
||||
)
|
||||
audioGroups.getOrNull(indexToFind)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Timber.v("Chosen audio ($audioIndex/$indexToFind) track: $chosenTrack")
|
||||
chosenTrack?.let {
|
||||
paramsBuilder
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||
|
|
@ -142,76 +132,8 @@ object TrackSelectionUtils {
|
|||
}
|
||||
return TrackSelectionResult(paramsBuilder.build(), audioSelected, subtitleSelected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the server provided index to the track index based on the [PlayerBackend] and other stream information
|
||||
*/
|
||||
private fun calculateIndexToFind(
|
||||
serverIndex: Int,
|
||||
type: MediaStreamType,
|
||||
playerBackend: PlayerBackend,
|
||||
embeddedSubtitleCount: Int,
|
||||
externalSubtitleCount: Int,
|
||||
subtitleIsExternal: Boolean,
|
||||
actualEmbeddedCount: Int?,
|
||||
source: MediaSourceInfo,
|
||||
): Int =
|
||||
when (playerBackend) {
|
||||
PlayerBackend.EXO_PLAYER,
|
||||
PlayerBackend.UNRECOGNIZED,
|
||||
-> {
|
||||
serverIndex - externalSubtitleCount + 1
|
||||
}
|
||||
|
||||
// TODO MPV could use literal indexes because they are stored in the track format ID
|
||||
PlayerBackend.PREFER_MPV,
|
||||
PlayerBackend.MPV,
|
||||
-> {
|
||||
when (type) {
|
||||
MediaStreamType.VIDEO -> {
|
||||
serverIndex - externalSubtitleCount + 1
|
||||
}
|
||||
|
||||
MediaStreamType.AUDIO -> {
|
||||
val videoStreamsBeforeAudioCount =
|
||||
source.mediaStreams
|
||||
.orEmpty()
|
||||
.indexOfFirst { it.type == MediaStreamType.AUDIO } - externalSubtitleCount
|
||||
serverIndex - externalSubtitleCount - videoStreamsBeforeAudioCount + 1
|
||||
}
|
||||
|
||||
MediaStreamType.SUBTITLE -> {
|
||||
if (subtitleIsExternal) {
|
||||
// Need to account for the actual embedded count because if the library
|
||||
// disables embedded subtitles, they still exist in the direct played file,
|
||||
// but not included in the MediaStreams list
|
||||
serverIndex + max(actualEmbeddedCount ?: 0, embeddedSubtitleCount) + 1
|
||||
} else {
|
||||
val videoStreamCount = source.videoStreamCount
|
||||
val audioStreamCount = source.audioStreamCount
|
||||
serverIndex - externalSubtitleCount - videoStreamCount - audioStreamCount + 1
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw UnsupportedOperationException("Cannot calculate index for $type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Format.idAsInt: Int?
|
||||
@OptIn(UnstableApi::class)
|
||||
get() =
|
||||
id?.let {
|
||||
if (it.contains(":")) {
|
||||
it.split(":").last().toIntOrNull()
|
||||
} else {
|
||||
it.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of external subtitle streams there are
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import androidx.media3.common.Format
|
|||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.github.damontecres.wholphin.ui.playback.idAsInt
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
|
|
@ -159,7 +158,7 @@ fun checkForSupport(tracks: Tracks): List<TrackSupport> =
|
|||
val reason = TrackSupportReason.fromInt(it.getTrackSupport(i))
|
||||
add(
|
||||
TrackSupport(
|
||||
format.id + " (${format.idAsInt})",
|
||||
format.id,
|
||||
type,
|
||||
reason,
|
||||
it.isSelected,
|
||||
|
|
|
|||
|
|
@ -235,6 +235,77 @@ class TestTrackSelection {
|
|||
return Tracks(groups)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the tracks for the `no_embedded_subs.json` for the given backend but skips a track ID
|
||||
*
|
||||
* Note: This is manual based on observation & code review of the playback for that file
|
||||
*/
|
||||
private fun buildMissingIdTracks(backend: PlayerBackend): Tracks {
|
||||
val formats =
|
||||
if (backend == PlayerBackend.MPV) {
|
||||
val video =
|
||||
Format
|
||||
.Builder()
|
||||
.setId("0:1")
|
||||
.setSampleMimeType("video/default")
|
||||
.build()
|
||||
val audios =
|
||||
(1..3).map {
|
||||
Format
|
||||
.Builder()
|
||||
.setId("$it:$it")
|
||||
.setSampleMimeType("audio/default")
|
||||
.build()
|
||||
}
|
||||
val subtitles =
|
||||
(1..3).map {
|
||||
Format
|
||||
.Builder()
|
||||
.setId("${it + 3}:$it")
|
||||
.setSampleMimeType("text/default")
|
||||
.build()
|
||||
}
|
||||
(listOf(video) + audios + subtitles)
|
||||
} else {
|
||||
// ExoPlayer
|
||||
val video =
|
||||
Format
|
||||
.Builder()
|
||||
.setId("0:1")
|
||||
.setSampleMimeType("video/default")
|
||||
.build()
|
||||
val audios =
|
||||
(3..5).map {
|
||||
Format
|
||||
.Builder()
|
||||
.setId("0:$it")
|
||||
.setSampleMimeType("audio/default")
|
||||
.build()
|
||||
}
|
||||
val subtitles =
|
||||
(6..8).map {
|
||||
Format
|
||||
.Builder()
|
||||
.setId("0:$it")
|
||||
.setSampleMimeType("text/default")
|
||||
.build()
|
||||
}
|
||||
(listOf(video) + audios + subtitles)
|
||||
}
|
||||
val groups =
|
||||
formats
|
||||
.map { TrackGroup(it) }
|
||||
.map {
|
||||
Tracks.Group(
|
||||
it,
|
||||
false,
|
||||
intArrayOf(C.FORMAT_HANDLED),
|
||||
booleanArrayOf(false),
|
||||
)
|
||||
}
|
||||
return Tracks(groups)
|
||||
}
|
||||
|
||||
private fun TrackSelectionParameters.getAudioOverride(): Format? {
|
||||
this.overrides.forEach { (trackGroup, trackSelectionOverride) ->
|
||||
if (trackGroup.type == C.TRACK_TYPE_AUDIO) {
|
||||
|
|
@ -588,4 +659,66 @@ class TestTrackSelection {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ExoPlayer missing ids`() {
|
||||
val resource = javaClass.classLoader?.getResource("no_embedded_subs.json")
|
||||
Assert.assertNotNull(resource)
|
||||
val fileContents = Paths.get(resource!!.toURI()).readText()
|
||||
val source = Json.decodeFromString<MediaSourceInfo>(fileContents)
|
||||
val tracks = buildMissingIdTracks(PlayerBackend.EXO_PLAYER)
|
||||
val ids = tracks.groups.flatMap { g -> (0..<g.length).map { g.getTrackFormat(it).id } }
|
||||
Assert.assertTrue("0:1" in ids)
|
||||
Assert.assertTrue("0:2" !in ids)
|
||||
Assert.assertTrue("0:3" in ids)
|
||||
|
||||
Assert.assertEquals(5, source.mediaStreams?.size)
|
||||
|
||||
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||
|
||||
TrackSelectionUtils
|
||||
.createTrackSelections(
|
||||
trackSelectionParams = trackSelectionParameters,
|
||||
tracks = tracks,
|
||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
||||
supportsDirectPlay = true,
|
||||
audioIndex = 3,
|
||||
subtitleIndex = TrackIndex.DISABLED,
|
||||
source = source,
|
||||
).also { result ->
|
||||
Assert.assertTrue(result.bothSelected)
|
||||
Assert.assertEquals("0:4", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||
Assert.assertEquals(null, result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test MPV missing ids`() {
|
||||
val resource = javaClass.classLoader?.getResource("no_embedded_subs.json")
|
||||
Assert.assertNotNull(resource)
|
||||
val fileContents = Paths.get(resource!!.toURI()).readText()
|
||||
val source = Json.decodeFromString<MediaSourceInfo>(fileContents)
|
||||
val tracks = buildMissingIdTracks(PlayerBackend.MPV)
|
||||
val ids = tracks.groups.flatMap { g -> (0..<g.length).map { g.getTrackFormat(it).id } }
|
||||
println(ids)
|
||||
|
||||
Assert.assertEquals(5, source.mediaStreams?.size)
|
||||
|
||||
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||
|
||||
TrackSelectionUtils
|
||||
.createTrackSelections(
|
||||
trackSelectionParams = trackSelectionParameters,
|
||||
tracks = tracks,
|
||||
playerBackend = PlayerBackend.MPV,
|
||||
supportsDirectPlay = true,
|
||||
audioIndex = 3,
|
||||
subtitleIndex = TrackIndex.DISABLED,
|
||||
source = source,
|
||||
).also { result ->
|
||||
Assert.assertTrue(result.bothSelected)
|
||||
Assert.assertEquals("2:2", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||
Assert.assertEquals(null, result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue