Fix some track selection bugs (#649)

## Description
Fixes track selection issues with MPV. This was especially focused on
libraries that disable embedded subtitles, but also fixes issues where
the audio streams are listed before video streams.

Also includes test media source JSON and unit tests to verify many
scenarios.

### Related issues
Fixes #494
This commit is contained in:
Ray 2026-01-07 17:27:54 -05:00 committed by GitHub
parent 6906813bbc
commit d2e0d527dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1278 additions and 28 deletions

View file

@ -494,8 +494,9 @@ class PlaybackViewModel
if (externalSubtitle == null) {
val result =
withContext(Dispatchers.Main) {
TrackSelectionUtils.applyTrackSelections(
player,
TrackSelectionUtils.createTrackSelections(
onMain { player.trackSelectionParameters },
onMain { player.currentTracks },
playerBackend,
true,
audioIndex,
@ -504,6 +505,7 @@ class PlaybackViewModel
)
}
if (result.bothSelected) {
onMain { player.trackSelectionParameters = result.trackSelectionParameters }
// TODO lots of duplicate code in this block
Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex")
val itemPlayback =
@ -696,8 +698,9 @@ class PlaybackViewModel
Timber.v("onTracksChanged: $tracks")
if (tracks.groups.isNotEmpty()) {
val result =
TrackSelectionUtils.applyTrackSelections(
player,
TrackSelectionUtils.createTrackSelections(
player.trackSelectionParameters,
player.currentTracks,
playerBackend,
source.supportsDirectPlay,
audioIndex.takeIf { transcodeType == PlayMethod.DIRECT_PLAY },
@ -705,6 +708,8 @@ class PlaybackViewModel
source,
)
if (result.bothSelected) {
player.trackSelectionParameters =
result.trackSelectionParameters
player.removeListener(this)
}
viewModelScope.launchIO { loadSubtitleDelay() }

View file

@ -3,8 +3,9 @@ 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.Player
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 org.jellyfin.sdk.model.api.MediaSourceInfo
@ -12,24 +13,24 @@ 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)
fun applyTrackSelections(
player: Player,
fun createTrackSelections(
trackSelectionParams: TrackSelectionParameters,
tracks: Tracks,
playerBackend: PlayerBackend,
supportsDirectPlay: Boolean,
audioIndex: Int?,
subtitleIndex: Int?,
source: MediaSourceInfo,
): TrackSelectionResult {
val videoStreamCount = source.videoStreamCount
val audioStreamCount = source.audioStreamCount
val embeddedSubtitleCount = source.embeddedSubtitleCount
val externalSubtitleCount = source.externalSubtitlesCount
val paramsBuilder = player.trackSelectionParameters.buildUpon()
val tracks = player.currentTracks.groups
val paramsBuilder = trackSelectionParams.buildUpon()
val groups = tracks.groups
val subtitleSelected =
if (subtitleIndex != null && subtitleIndex >= 0) {
@ -37,7 +38,7 @@ object TrackSelectionUtils {
if (subtitleIsExternal || supportsDirectPlay) {
val chosenTrack =
if (subtitleIsExternal && playerBackend == PlayerBackend.EXO_PLAYER) {
tracks.firstOrNull { group ->
groups.firstOrNull { group ->
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
(0..<group.mediaTrackGroup.length)
.mapNotNull {
@ -45,23 +46,38 @@ object TrackSelectionUtils {
}.any { it.endsWith("e:$subtitleIndex") }
}
} 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,
videoStreamCount,
audioStreamCount,
embeddedSubtitleCount,
externalSubtitleCount,
subtitleIsExternal,
actualEmbeddedCount,
source,
)
Timber.v("Chosen subtitle ($subtitleIndex/$indexToFind) track")
// subtitleIndex - externalSubtitleCount + 1
tracks.firstOrNull { group ->
groups.firstOrNull { group ->
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
(0..<group.mediaTrackGroup.length)
.map {
.filter {
if (subtitleIsExternal) {
group.getTrackFormat(0).id?.contains("e:") == true
} else {
group.getTrackFormat(0).id?.contains("e:") == false
}
}.map {
group.getTrackFormat(it).idAsInt
}.contains(indexToFind)
}
@ -95,14 +111,14 @@ object TrackSelectionUtils {
audioIndex,
MediaStreamType.AUDIO,
playerBackend,
videoStreamCount,
audioStreamCount,
embeddedSubtitleCount,
externalSubtitleCount,
false,
null,
source,
)
val chosenTrack =
tracks.firstOrNull { group ->
groups.firstOrNull { group ->
group.type == C.TRACK_TYPE_AUDIO && group.isSupported &&
(0..<group.mediaTrackGroup.length)
.map {
@ -124,10 +140,7 @@ object TrackSelectionUtils {
} else {
audioIndex == null
}
if (audioSelected && subtitleSelected) {
player.trackSelectionParameters = paramsBuilder.build()
}
return TrackSelectionResult(audioSelected, subtitleSelected)
return TrackSelectionResult(paramsBuilder.build(), audioSelected, subtitleSelected)
}
/**
@ -137,11 +150,11 @@ object TrackSelectionUtils {
serverIndex: Int,
type: MediaStreamType,
playerBackend: PlayerBackend,
videoStreamCount: Int,
audioStreamCount: Int,
embeddedSubtitleCount: Int,
externalSubtitleCount: Int,
subtitleIsExternal: Boolean,
actualEmbeddedCount: Int?,
source: MediaSourceInfo,
): Int =
when (playerBackend) {
PlayerBackend.EXO_PLAYER,
@ -158,13 +171,22 @@ object TrackSelectionUtils {
}
MediaStreamType.AUDIO -> {
serverIndex - externalSubtitleCount - videoStreamCount + 1
val videoStreamsBeforeAudioCount =
source.mediaStreams
.orEmpty()
.indexOfFirst { it.type == MediaStreamType.AUDIO } - externalSubtitleCount
serverIndex - externalSubtitleCount - videoStreamsBeforeAudioCount + 1
}
MediaStreamType.SUBTITLE -> {
if (subtitleIsExternal) {
serverIndex + embeddedSubtitleCount + 1
// 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
}
}
@ -228,12 +250,14 @@ fun MediaSourceInfo.findExternalSubtitle(subtitleIndex: Int?): MediaStream? = me
fun List<MediaStream>.findExternalSubtitle(subtitleIndex: Int?): MediaStream? =
subtitleIndex?.let {
firstOrNull {
it.type == MediaStreamType.SUBTITLE && it.deliveryMethod == SubtitleDeliveryMethod.EXTERNAL &&
it.type == MediaStreamType.SUBTITLE &&
(it.deliveryMethod == SubtitleDeliveryMethod.EXTERNAL || it.isExternal) &&
it.index == subtitleIndex
}
}
data class TrackSelectionResult(
val trackSelectionParameters: TrackSelectionParameters,
val audioSelected: Boolean,
val subtitleSelected: Boolean,
) {

View file

@ -0,0 +1,9 @@
package android.text;
// Mocks static class for non-instrumented unit tests
public class TextUtils {
public static boolean isEmpty(CharSequence str) {
return str == null || str.length() == 0;
}
}

View file

@ -0,0 +1,591 @@
package com.github.damontecres.wholphin.test
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.TrackGroup
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks
import com.github.damontecres.wholphin.data.model.TrackIndex
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.ui.playback.TrackSelectionUtils
import kotlinx.serialization.json.Json
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.junit.Assert
import org.junit.Test
import java.nio.file.Paths
import kotlin.io.path.readText
class TestTrackSelection {
/**
* Builds the tracks for the `embedded_subs.json` for the given backend
*
* Note: This is manual based on observation & code review of the playback for that file
*/
private fun buildEmbeddedTracks(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 {
val video =
Format
.Builder()
.setId("1")
.setSampleMimeType("video/default")
.build()
val audios =
(2..4).map {
Format
.Builder()
.setId("$it")
.setSampleMimeType("audio/default")
.build()
}
val subtitles =
(5..7).map {
Format
.Builder()
.setId("$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)
}
/**
* Builds the tracks for the `no_embedded_subs.json` for the given backend
*
* Note: This is manual based on observation & code review of the playback for that file
*/
private fun buildNoEmbeddedTracks(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(
Format
.Builder()
.setId("7:e:4")
.setSampleMimeType("text/default")
.build(),
)
(listOf(video) + audios + subtitles)
} else {
// ExoPlayer
val video =
Format
.Builder()
.setId("0:1")
.setSampleMimeType("video/default")
.build()
val audios =
(2..4).map {
Format
.Builder()
.setId("0:$it")
.setSampleMimeType("audio/default")
.build()
}
val subtitles =
(5..7).map {
Format
.Builder()
.setId("0:$it")
.setSampleMimeType("text/default")
.build()
} +
listOf(
Format
.Builder()
.setId("1:e:0")
.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)
}
/**
* Builds the tracks for the `external_subs.json` for the given backend.
*
* Must supply the desired subtitle index because ExoPlayer uses it.
*
* Note: This is manual based on observation & code review of the playback for that file
*/
private fun buildExternalTracks(
backend: PlayerBackend,
selectedIndex: Int,
): Tracks {
val formats =
if (backend == PlayerBackend.MPV) {
val video =
Format
.Builder()
.setId("1:1")
.setSampleMimeType("video/default")
.build()
val audios =
listOf(
Format
.Builder()
.setId("0:1")
.setSampleMimeType("audio/default")
.build(),
)
val subtitles =
listOf(
Format
.Builder()
.setId("2:1")
.setSampleMimeType("text/default")
.build(),
Format
.Builder()
.setId("3:e:2")
.setSampleMimeType("text/default")
.build(),
)
(listOf(video) + audios + subtitles)
} else {
val video =
Format
.Builder()
.setId("0:2")
.setSampleMimeType("video/default")
.build()
val audios =
listOf(
Format
.Builder()
.setId("0:1")
.setSampleMimeType("audio/default")
.build(),
)
val subtitles =
listOf(
Format
.Builder()
.setId("0:3") // Embedded
.setSampleMimeType("text/default")
.build(),
Format
.Builder()
.setId("1:e:$selectedIndex") // External
.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) {
return trackGroup.getFormat(trackSelectionOverride.trackIndices.first())
}
}
return null
}
private fun TrackSelectionParameters.getSubtitleOverride(): Format? {
this.overrides.forEach { (trackGroup, trackSelectionOverride) ->
if (trackGroup.type == C.TRACK_TYPE_TEXT) {
return trackGroup.getFormat(trackSelectionOverride.trackIndices.first())
}
}
return null
}
@Test
fun `test MPV embedded`() {
val resource = javaClass.classLoader?.getResource("embedded_subs.json")
Assert.assertNotNull(resource)
val fileContents = Paths.get(resource!!.toURI()).readText()
val source = Json.decodeFromString<MediaSourceInfo>(fileContents)
val tracks = buildEmbeddedTracks(PlayerBackend.MPV)
Assert.assertEquals(7, source.mediaStreams?.size)
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.MPV,
supportsDirectPlay = true,
audioIndex = 1,
subtitleIndex = 4,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("1:1", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("4:1", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.MPV,
supportsDirectPlay = true,
audioIndex = 2,
subtitleIndex = 4,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("2:2", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("4:1", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.MPV,
supportsDirectPlay = true,
audioIndex = 1,
subtitleIndex = TrackIndex.DISABLED,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("1:1", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals(null, result.trackSelectionParameters.getSubtitleOverride()?.id)
}
}
@Test
fun `test ExoPlayer embedded`() {
val resource = javaClass.classLoader?.getResource("embedded_subs.json")
Assert.assertNotNull(resource)
val fileContents = Paths.get(resource!!.toURI()).readText()
val source = Json.decodeFromString<MediaSourceInfo>(fileContents)
val tracks = buildEmbeddedTracks(PlayerBackend.EXO_PLAYER)
Assert.assertEquals(7, source.mediaStreams?.size)
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 1,
subtitleIndex = 4,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("2", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("5", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 2,
subtitleIndex = 4,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("3", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("5", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 2,
subtitleIndex = 6,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("3", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("7", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 1,
subtitleIndex = TrackIndex.DISABLED,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("2", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals(null, result.trackSelectionParameters.getSubtitleOverride()?.id)
}
}
@Test
fun `test MPV no embedded`() {
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 = buildNoEmbeddedTracks(PlayerBackend.MPV)
Assert.assertEquals(5, source.mediaStreams?.size)
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.MPV,
supportsDirectPlay = true,
audioIndex = 2,
subtitleIndex = 0,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("1:1", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("7:e:4", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.MPV,
supportsDirectPlay = true,
audioIndex = 3,
subtitleIndex = 0,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("2:2", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("7:e:4", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
}
@Test
fun `test ExoPlayer no embedded`() {
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 = buildNoEmbeddedTracks(PlayerBackend.EXO_PLAYER)
Assert.assertEquals(5, source.mediaStreams?.size)
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 2,
subtitleIndex = 0,
source = source,
).also { result ->
Assert.assertTrue(result.bothSelected)
Assert.assertEquals("0:2", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("1:e:0", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
}
@Test
fun `test MPV external`() {
val resource = javaClass.classLoader?.getResource("external_subs.json")
Assert.assertNotNull(resource)
val fileContents = Paths.get(resource!!.toURI()).readText()
val source = Json.decodeFromString<MediaSourceInfo>(fileContents)
val tracks = buildExternalTracks(PlayerBackend.MPV, 0)
Assert.assertEquals(6, source.mediaStreams?.size)
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.MPV,
supportsDirectPlay = true,
audioIndex = 3,
subtitleIndex = 0,
source = source,
).also { result ->
Assert.assertTrue(result.audioSelected)
Assert.assertTrue(result.subtitleSelected)
Assert.assertEquals("0:1", result.trackSelectionParameters.getAudioOverride()?.id)
Assert.assertEquals("3:e:2", result.trackSelectionParameters.getSubtitleOverride()?.id)
}
// Select embedded subtitles
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.MPV,
supportsDirectPlay = true,
audioIndex = 3,
subtitleIndex = 5,
source = source,
).also { result ->
Assert.assertTrue(result.audioSelected)
Assert.assertTrue(result.subtitleSelected)
Assert.assertEquals(
"0:1",
result.trackSelectionParameters.getAudioOverride()?.id,
)
Assert.assertEquals(
"2:1",
result.trackSelectionParameters.getSubtitleOverride()?.id,
)
}
}
@Test
fun `test ExoPlayer external`() {
val resource = javaClass.classLoader?.getResource("external_subs.json")
Assert.assertNotNull(resource)
val fileContents = Paths.get(resource!!.toURI()).readText()
val source = Json.decodeFromString<MediaSourceInfo>(fileContents)
buildExternalTracks(PlayerBackend.EXO_PLAYER, 0).also { tracks ->
Assert.assertEquals(6, source.mediaStreams?.size)
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 3,
subtitleIndex = 0,
source = source,
).also { result ->
Assert.assertTrue(result.audioSelected)
Assert.assertTrue(result.subtitleSelected)
Assert.assertEquals(
"0:1",
result.trackSelectionParameters.getAudioOverride()?.id,
)
Assert.assertEquals(
"1:e:0",
result.trackSelectionParameters.getSubtitleOverride()?.id,
)
}
// Select embedded subtitles
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 3,
subtitleIndex = 5,
source = source,
).also { result ->
Assert.assertTrue(result.audioSelected)
Assert.assertTrue(result.subtitleSelected)
Assert.assertEquals(
"0:1",
result.trackSelectionParameters.getAudioOverride()?.id,
)
Assert.assertEquals(
"0:3",
result.trackSelectionParameters.getSubtitleOverride()?.id,
)
}
}
buildExternalTracks(PlayerBackend.EXO_PLAYER, 2).also { tracks ->
Assert.assertEquals(6, source.mediaStreams?.size)
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
TrackSelectionUtils
.createTrackSelections(
trackSelectionParams = trackSelectionParameters,
tracks = tracks,
playerBackend = PlayerBackend.EXO_PLAYER,
supportsDirectPlay = true,
audioIndex = 3,
subtitleIndex = 2,
source = source,
).also { result ->
Assert.assertTrue(result.audioSelected)
Assert.assertTrue(result.subtitleSelected)
Assert.assertEquals(
"0:1",
result.trackSelectionParameters.getAudioOverride()?.id,
)
Assert.assertEquals(
"1:e:2",
result.trackSelectionParameters.getSubtitleOverride()?.id,
)
}
}
}
}

View file

@ -0,0 +1,235 @@
{
"Protocol": "File",
"Id": "",
"Path": "",
"Type": "Default",
"Container": "mkv",
"Size": 651217902,
"Name": "",
"IsRemote": false,
"ETag": "",
"RunTimeTicks": 14919360000,
"ReadAtNativeFramerate": false,
"IgnoreDts": false,
"IgnoreIndex": false,
"GenPtsInput": false,
"SupportsTranscoding": true,
"SupportsDirectStream": true,
"SupportsDirectPlay": true,
"IsInfiniteStream": false,
"UseMostCompatibleTranscodingProfile": false,
"RequiresOpening": false,
"RequiresClosing": false,
"RequiresLooping": false,
"SupportsProbing": true,
"VideoType": "VideoFile",
"MediaStreams": [
{
"Codec": "hevc",
"Language": "jpn",
"TimeBase": "1/1000",
"Title": "",
"VideoRange": "SDR",
"VideoRangeType": "SDR",
"AudioSpatialFormat": "None",
"DisplayTitle": "1080p - HEVC - SDR",
"IsInterlaced": false,
"IsAVC": false,
"BitRate": 3491934,
"BitDepth": 10,
"RefFrames": 1,
"IsDefault": true,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 1080,
"Width": 1448,
"AverageFrameRate": 23.809525,
"RealFrameRate": 23.809525,
"ReferenceFrameRate": 23.809525,
"Profile": "Main 10",
"Type": "Video",
"AspectRatio": "4:3",
"Index": 0,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"PixelFormat": "yuv420p10le",
"Level": 120,
"IsAnamorphic": false
},
{
"Codec": "opus",
"Language": "jpn",
"TimeBase": "1/1000",
"Title": "JAP Stereo (Opus 112Kbps)",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedDefault": "Default",
"LocalizedExternal": "External",
"DisplayTitle": "JAP Stereo (Opus 112Kbps) - Japanese",
"IsInterlaced": false,
"IsAVC": false,
"ChannelLayout": "stereo",
"BitRate": 101618,
"Channels": 2,
"SampleRate": 48000,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Type": "Audio",
"Index": 1,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"Level": 0
},
{
"Codec": "aac",
"Language": "por",
"TimeBase": "1/1000",
"Title": "",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedDefault": "Default",
"LocalizedExternal": "External",
"DisplayTitle": "Portuguese - AAC - Stereo",
"IsInterlaced": false,
"IsAVC": false,
"ChannelLayout": "stereo",
"BitRate": 249225,
"Channels": 2,
"SampleRate": 44100,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Profile": "LC",
"Type": "Audio",
"Index": 2,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"Level": 0
},
{
"Codec": "eac3",
"Language": "por",
"TimeBase": "1/1000",
"Title": "",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedDefault": "Default",
"LocalizedExternal": "External",
"DisplayTitle": "Portuguese - Dolby Digital+ - 5.1",
"IsInterlaced": false,
"IsAVC": false,
"ChannelLayout": "5.1",
"BitRate": 640000,
"Channels": 6,
"SampleRate": 48000,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Type": "Audio",
"Index": 3,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"Level": 0
},
{
"Codec": "ass",
"Language": "por",
"TimeBase": "1/1000",
"Title": "ptBR",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "ptBR - Portuguese - Default - ASS",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": true,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 4,
"IsExternal": false,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Level": 0
},
{
"Codec": "ass",
"Language": "por",
"TimeBase": "1/1000",
"Title": "ptBR FORCED",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "ptBR FORCED - Portuguese - ASS",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": false,
"IsForced": true,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 5,
"IsExternal": false,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Level": 0
},
{
"Codec": "ass",
"Language": "eng",
"TimeBase": "1/1000",
"Title": "enUS",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "enUS - English - ASS",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 6,
"IsExternal": false,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Level": 0
}
],
"MediaAttachments": [],
"Formats": [],
"Bitrate": 4482777,
"RequiredHttpHeaders": {},
"TranscodingSubProtocol": "http",
"DefaultAudioStreamIndex": 1,
"DefaultSubtitleStreamIndex": 6,
"HasSegments": true
}

View file

@ -0,0 +1,208 @@
{
"Protocol": "File",
"Id": "",
"Path": "",
"Type": "Default",
"Container": "mkv",
"Size": 2147179830,
"Name": "",
"IsRemote": false,
"ETag": "",
"RunTimeTicks": 12793200000,
"ReadAtNativeFramerate": false,
"IgnoreDts": false,
"IgnoreIndex": false,
"GenPtsInput": false,
"SupportsTranscoding": true,
"SupportsDirectStream": true,
"SupportsDirectPlay": true,
"IsInfiniteStream": false,
"UseMostCompatibleTranscodingProfile": false,
"RequiresOpening": false,
"RequiresClosing": false,
"RequiresLooping": false,
"SupportsProbing": true,
"VideoType": "VideoFile",
"MediaStreams": [
{
"Codec": "subrip",
"Language": "eng",
"TimeBase": "1/1000",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "English - SUBRIP - External",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 0,
"IsExternal": true,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Path": "",
"Level": 0
},
{
"Codec": "subrip",
"Language": "eng",
"TimeBase": "1/1000",
"Title": "0",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "0 - English - SUBRIP - External",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 1,
"IsExternal": true,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Path": "",
"Level": 0
},
{
"Codec": "subrip",
"Language": "eng",
"TimeBase": "1/1000",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "English - SUBRIP - External",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 2,
"IsExternal": true,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Path": "",
"Level": 0
},
{
"Codec": "eac3",
"Language": "eng",
"TimeBase": "1/1000",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedDefault": "Default",
"LocalizedExternal": "External",
"DisplayTitle": "English - Dolby Digital+ - 5.1 - Default",
"IsInterlaced": false,
"IsAVC": false,
"ChannelLayout": "5.1",
"BitRate": 640000,
"Channels": 6,
"SampleRate": 48000,
"IsDefault": true,
"IsForced": false,
"IsHearingImpaired": false,
"Type": "Audio",
"Index": 3,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"Level": 0
},
{
"Codec": "h264",
"ColorSpace": "bt709",
"ColorTransfer": "bt709",
"ColorPrimaries": "bt709",
"TimeBase": "1/1000",
"VideoRange": "SDR",
"VideoRangeType": "SDR",
"AudioSpatialFormat": "None",
"DisplayTitle": "1080p H264 SDR",
"NalLengthSize": "4",
"IsInterlaced": false,
"IsAVC": true,
"BitRate": 13427007,
"BitDepth": 8,
"RefFrames": 1,
"IsDefault": true,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 1080,
"Width": 1920,
"AverageFrameRate": 23.976025,
"RealFrameRate": 23.976025,
"ReferenceFrameRate": 23.976025,
"Profile": "High",
"Type": "Video",
"AspectRatio": "16:9",
"Index": 4,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"PixelFormat": "yuv420p",
"Level": 40,
"IsAnamorphic": false
},
{
"Codec": "subrip",
"TimeBase": "1/1000",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "Undefined - Default - SUBRIP",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": true,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 5,
"IsExternal": false,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Level": 0
}
],
"MediaAttachments": [],
"Formats": [],
"Bitrate": 14067007,
"RequiredHttpHeaders": {},
"TranscodingSubProtocol": "http",
"DefaultAudioStreamIndex": 3,
"DefaultSubtitleStreamIndex": 0,
"HasSegments": true
}

View file

@ -0,0 +1,178 @@
{
"Protocol": "File",
"Id": "",
"Path": "",
"Type": "Default",
"Container": "mkv",
"Size": 651217902,
"Name": "",
"IsRemote": false,
"ETag": "",
"RunTimeTicks": 14919360000,
"ReadAtNativeFramerate": false,
"IgnoreDts": false,
"IgnoreIndex": false,
"GenPtsInput": false,
"SupportsTranscoding": true,
"SupportsDirectStream": true,
"SupportsDirectPlay": true,
"IsInfiniteStream": false,
"UseMostCompatibleTranscodingProfile": false,
"RequiresOpening": false,
"RequiresClosing": false,
"RequiresLooping": false,
"SupportsProbing": true,
"VideoType": "VideoFile",
"MediaStreams": [
{
"Codec": "subrip",
"Language": "spa",
"TimeBase": "1/1000",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedUndefined": "Undefined",
"LocalizedDefault": "Default",
"LocalizedForced": "Forced",
"LocalizedExternal": "External",
"LocalizedHearingImpaired": "Hearing Impaired",
"DisplayTitle": "Spanish - SUBRIP - External",
"IsInterlaced": false,
"IsAVC": false,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 0,
"Width": 0,
"Type": "Subtitle",
"Index": 0,
"IsExternal": true,
"IsTextSubtitleStream": true,
"SupportsExternalStream": true,
"Path": "",
"Level": 0
},
{
"Codec": "hevc",
"Language": "jpn",
"TimeBase": "1/1000",
"Title": "",
"VideoRange": "SDR",
"VideoRangeType": "SDR",
"AudioSpatialFormat": "None",
"DisplayTitle": "1080p - HEVC - SDR",
"IsInterlaced": false,
"IsAVC": false,
"BitRate": 3491934,
"BitDepth": 10,
"RefFrames": 1,
"IsDefault": true,
"IsForced": false,
"IsHearingImpaired": false,
"Height": 1080,
"Width": 1448,
"AverageFrameRate": 23.809525,
"RealFrameRate": 23.809525,
"ReferenceFrameRate": 23.809525,
"Profile": "Main 10",
"Type": "Video",
"AspectRatio": "4:3",
"Index": 1,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"PixelFormat": "yuv420p10le",
"Level": 120,
"IsAnamorphic": false
},
{
"Codec": "opus",
"Language": "jpn",
"TimeBase": "1/1000",
"Title": "Stereo (Opus 112Kbps)",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedDefault": "Default",
"LocalizedExternal": "External",
"DisplayTitle": "Stereo (Opus 112Kbps) - Japanese",
"IsInterlaced": false,
"IsAVC": false,
"ChannelLayout": "stereo",
"BitRate": 101618,
"Channels": 2,
"SampleRate": 48000,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Type": "Audio",
"Index": 2,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"Level": 0
},
{
"Codec": "aac",
"Language": "por",
"TimeBase": "1/1000",
"Title": "",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedDefault": "Default",
"LocalizedExternal": "External",
"DisplayTitle": "Portuguese - AAC - Stereo",
"IsInterlaced": false,
"IsAVC": false,
"ChannelLayout": "stereo",
"BitRate": 249225,
"Channels": 2,
"SampleRate": 44100,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Profile": "LC",
"Type": "Audio",
"Index": 3,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"Level": 0
},
{
"Codec": "eac3",
"Language": "por",
"TimeBase": "1/1000",
"Title": "",
"VideoRange": "Unknown",
"VideoRangeType": "Unknown",
"AudioSpatialFormat": "None",
"LocalizedDefault": "Default",
"LocalizedExternal": "External",
"DisplayTitle": "Portuguese - Dolby Digital+ - 5.1",
"IsInterlaced": false,
"IsAVC": false,
"ChannelLayout": "5.1",
"BitRate": 640000,
"Channels": 6,
"SampleRate": 48000,
"IsDefault": false,
"IsForced": false,
"IsHearingImpaired": false,
"Type": "Audio",
"Index": 4,
"IsExternal": false,
"IsTextSubtitleStream": false,
"SupportsExternalStream": false,
"Level": 0
}
],
"MediaAttachments": [],
"Formats": [],
"Bitrate": 4482777,
"RequiredHttpHeaders": {},
"TranscodingSubProtocol": "http",
"DefaultAudioStreamIndex": 2,
"HasSegments": true
}