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 1e56ce08..d8190c30 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 @@ -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() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt index 44a44e1e..4c952a20 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt @@ -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.type == C.TRACK_TYPE_TEXT && + (0.. + groups.firstOrNull { group -> group.type == C.TRACK_TYPE_TEXT && group.isSupported && (0.. + groups.firstOrNull { group -> group.type == C.TRACK_TYPE_AUDIO && group.isSupported && (0.. { - 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.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, ) { diff --git a/app/src/test/java/android/text/TextUtils.java b/app/src/test/java/android/text/TextUtils.java new file mode 100644 index 00000000..959d7957 --- /dev/null +++ b/app/src/test/java/android/text/TextUtils.java @@ -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; + } +} diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestTrackSelection.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestTrackSelection.kt new file mode 100644 index 00000000..cad496ca --- /dev/null +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestTrackSelection.kt @@ -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(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(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(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(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(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(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, + ) + } + } + } +} diff --git a/app/src/test/resources/embedded_subs.json b/app/src/test/resources/embedded_subs.json new file mode 100644 index 00000000..f24aeefc --- /dev/null +++ b/app/src/test/resources/embedded_subs.json @@ -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 +} diff --git a/app/src/test/resources/external_subs.json b/app/src/test/resources/external_subs.json new file mode 100644 index 00000000..9f6a4b05 --- /dev/null +++ b/app/src/test/resources/external_subs.json @@ -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 +} diff --git a/app/src/test/resources/no_embedded_subs.json b/app/src/test/resources/no_embedded_subs.json new file mode 100644 index 00000000..be4e3051 --- /dev/null +++ b/app/src/test/resources/no_embedded_subs.json @@ -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 +}