mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-07 03:04:50 +01:00
Improve track selection logic (#1654)
## Description Updates the logic for selecting tracks which should fix several edge cases The new logic groups track types together and then applying the user's selection to the actual tracks in the file. For example, if the user choosing the audio track which the server reports as absolute `index=2` (eg the _third_ track of any type in the file) and audio `index=1` (eg the _second audio_ track in the file), then the track selection logic will use the _second audio_ track in the direct played file (after accounting for id sorting). Some other changes: - If direct playing and an unsupported track is selected, the app will now fall back to transcoding - If the correct audio/subtitle tracks are not found, the app will now fall back to transcoding instead of blocking any track change attempts ### TODO Not necessarily in this PR, but some things need further investigation: - Handling external audio tracks - Why does ExoPlayer order the tracks differently sometimes, see [example here](https://github.com/damontecres/Wholphin/issues/791#issuecomment-4836470813) where `0:3` is _after_ `0:4`, but the `MediaSourceInfo` lists them `0:3` then `0:4` ### Related issues Maybe fixes #791 Should fix https://github.com/damontecres/Wholphin/issues/1005#issuecomment-4085440175 Related to the parent issue, #1475 Replaces #845 ### Testing Unit tests mostly, some basic tests on emulator Needs more "real world" testing #### Try it out? You can try out this build by following the [instructions here](https://github.com/damontecres/Wholphin/releases/tag/develop-track-selection). If you do, please report your results in this PR! ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
d481aea85a
commit
26124a5348
10 changed files with 1014 additions and 498 deletions
|
|
@ -41,6 +41,7 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
||||||
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.enabled
|
||||||
import com.github.damontecres.wholphin.services.DatePlayedService
|
import com.github.damontecres.wholphin.services.DatePlayedService
|
||||||
import com.github.damontecres.wholphin.services.DeviceProfileService
|
import com.github.damontecres.wholphin.services.DeviceProfileService
|
||||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||||
|
|
@ -614,11 +615,16 @@ class PlaybackViewModel
|
||||||
userInitiated: Boolean,
|
userInitiated: Boolean,
|
||||||
enableDirectPlay: Boolean = !this.forceTranscoding,
|
enableDirectPlay: Boolean = !this.forceTranscoding,
|
||||||
enableDirectStream: Boolean = !this.forceTranscoding,
|
enableDirectStream: Boolean = !this.forceTranscoding,
|
||||||
) = withContext(WholphinDispatchers.IO) {
|
): Unit =
|
||||||
|
withContext(WholphinDispatchers.IO) {
|
||||||
val itemId = item.id
|
val itemId = item.id
|
||||||
|
|
||||||
val currentPlayback = state.value.currentPlayback
|
val currentPlayback = state.value.currentPlayback
|
||||||
if (currentPlayback != null && currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) {
|
if (currentPlayback != null &&
|
||||||
|
currentPlayback.item.id == item.id &&
|
||||||
|
currentPlayback.playMethod == PlayMethod.DIRECT_PLAY &&
|
||||||
|
enableDirectPlay
|
||||||
|
) {
|
||||||
val wasSuccessful =
|
val wasSuccessful =
|
||||||
changeStreamsDirectPlay(
|
changeStreamsDirectPlay(
|
||||||
currentPlayback = currentPlayback,
|
currentPlayback = currentPlayback,
|
||||||
|
|
@ -630,9 +636,15 @@ class PlaybackViewModel
|
||||||
if (wasSuccessful) return@withContext
|
if (wasSuccessful) return@withContext
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d(
|
Timber.i(
|
||||||
"changeStreams: userInitiated=$userInitiated, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex, " +
|
"changeStreams (%s): userInitiated=%s, audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s",
|
||||||
"enableDirectPlay=$enableDirectPlay, enableDirectStream=$enableDirectStream, positionMs=$positionMs",
|
itemId,
|
||||||
|
userInitiated,
|
||||||
|
audioIndex,
|
||||||
|
subtitleIndex,
|
||||||
|
enableDirectPlay,
|
||||||
|
enableDirectStream,
|
||||||
|
positionMs,
|
||||||
)
|
)
|
||||||
|
|
||||||
val maxBitrate =
|
val maxBitrate =
|
||||||
|
|
@ -736,7 +748,9 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("subtitleIndex=$subtitleIndex, externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle")
|
Timber.v(
|
||||||
|
"subtitleIndex=$subtitleIndex, externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle",
|
||||||
|
)
|
||||||
|
|
||||||
val mediaItem =
|
val mediaItem =
|
||||||
MediaItem
|
MediaItem
|
||||||
|
|
@ -808,7 +822,7 @@ class PlaybackViewModel
|
||||||
mediaItem,
|
mediaItem,
|
||||||
positionMs,
|
positionMs,
|
||||||
)
|
)
|
||||||
if (audioIndex != null || subtitleIndex != null) {
|
if (transcodeType == PlayMethod.DIRECT_PLAY && (audioIndex != null || subtitleIndex != null)) {
|
||||||
val onTracksChangedListener =
|
val onTracksChangedListener =
|
||||||
object : Player.Listener {
|
object : Player.Listener {
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
|
|
@ -818,17 +832,30 @@ class PlaybackViewModel
|
||||||
TrackSelectionUtils.createTrackSelections(
|
TrackSelectionUtils.createTrackSelections(
|
||||||
player.trackSelectionParameters,
|
player.trackSelectionParameters,
|
||||||
player.currentTracks,
|
player.currentTracks,
|
||||||
currentPlayer.value!!.backend,
|
audioIndex,
|
||||||
source.supportsDirectPlay,
|
|
||||||
audioIndex.takeIf { transcodeType == PlayMethod.DIRECT_PLAY },
|
|
||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
Timber.v("onTracksChanged: %s", result)
|
Timber.v("onTracksChanged: %s", result)
|
||||||
|
player.removeListener(this)
|
||||||
if (result.bothSelected) {
|
if (result.bothSelected) {
|
||||||
player.trackSelectionParameters =
|
player.trackSelectionParameters =
|
||||||
result.trackSelectionParameters
|
result.trackSelectionParameters
|
||||||
player.removeListener(this)
|
} else {
|
||||||
|
// Fall back to transcoding
|
||||||
|
Timber.w("Failed to select tracks, falling back to transcoding")
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
changeStreams(
|
||||||
|
item = item,
|
||||||
|
currentItemPlayback = currentItemPlayback,
|
||||||
|
audioIndex = audioIndex,
|
||||||
|
subtitleIndex = subtitleIndex,
|
||||||
|
positionMs = positionMs,
|
||||||
|
userInitiated = userInitiated,
|
||||||
|
enableDirectPlay = false,
|
||||||
|
enableDirectStream = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO { loadSubtitleDelay() }
|
viewModelScope.launchIO { loadSubtitleDelay() }
|
||||||
}
|
}
|
||||||
|
|
@ -853,12 +880,11 @@ class PlaybackViewModel
|
||||||
userInitiated: Boolean,
|
userInitiated: Boolean,
|
||||||
): Boolean =
|
): Boolean =
|
||||||
withContext(WholphinDispatchers.IO) {
|
withContext(WholphinDispatchers.IO) {
|
||||||
// TODO there's probably no reason why we can't add external subtitles?
|
|
||||||
Timber.v("changeStreams direct play")
|
Timber.v("changeStreams direct play")
|
||||||
|
|
||||||
// TODO Better way to handle unsupported types in general is needed
|
// TODO Better way to handle unsupported types in general is needed
|
||||||
// This is a workaround for switching to a non AC3 track when the user wants audio transcoded to AC3
|
// This is a workaround for switching to a non AC3 track when the user wants audio transcoded to AC3
|
||||||
if (preferences.appPreferences.experimentalPreferences.preferAc3Surround && audioIndex != null) {
|
if (preferences.appPreferences.experimentalPreferences.enabled { preferAc3Surround } && audioIndex != null) {
|
||||||
currentPlayback.mediaSourceInfo.mediaStreams
|
currentPlayback.mediaSourceInfo.mediaStreams
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.firstOrNull { it.index == audioIndex }
|
.firstOrNull { it.index == audioIndex }
|
||||||
|
|
@ -873,15 +899,12 @@ class PlaybackViewModel
|
||||||
val source = currentPlayback.mediaSourceInfo
|
val source = currentPlayback.mediaSourceInfo
|
||||||
val externalSubtitle = source.findExternalSubtitle(subtitleIndex)
|
val externalSubtitle = source.findExternalSubtitle(subtitleIndex)
|
||||||
|
|
||||||
// TODO there's probably no reason why we can't add external subtitles?
|
|
||||||
if (externalSubtitle == null) {
|
if (externalSubtitle == null) {
|
||||||
val result =
|
val result =
|
||||||
withContext(WholphinDispatchers.Main) {
|
withContext(WholphinDispatchers.Main) {
|
||||||
TrackSelectionUtils.createTrackSelections(
|
TrackSelectionUtils.createTrackSelections(
|
||||||
onMain { player.trackSelectionParameters },
|
onMain { player.trackSelectionParameters },
|
||||||
onMain { player.currentTracks },
|
onMain { player.currentTracks },
|
||||||
currentPlayer.value!!.backend,
|
|
||||||
true,
|
|
||||||
audioIndex,
|
audioIndex,
|
||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
source,
|
source,
|
||||||
|
|
@ -916,7 +939,7 @@ class PlaybackViewModel
|
||||||
it.copy(
|
it.copy(
|
||||||
currentPlayback =
|
currentPlayback =
|
||||||
(it.currentPlayback ?: currentPlayback).copy(
|
(it.currentPlayback ?: currentPlayback).copy(
|
||||||
tracks = checkForSupport(player.currentTracks),
|
tracks = checkForSupport(onMain { player.currentTracks }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,11 @@ import androidx.media3.common.TrackSelectionOverride
|
||||||
import androidx.media3.common.TrackSelectionParameters
|
import androidx.media3.common.TrackSelectionParameters
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functions for selecting which audio & subtitle tracks to activate in the [androidx.media3.common.Player]
|
* Functions for selecting which audio & subtitle tracks to activate in the [androidx.media3.common.Player]
|
||||||
|
|
@ -23,200 +21,151 @@ object TrackSelectionUtils {
|
||||||
fun createTrackSelections(
|
fun createTrackSelections(
|
||||||
trackSelectionParams: TrackSelectionParameters,
|
trackSelectionParams: TrackSelectionParameters,
|
||||||
tracks: Tracks,
|
tracks: Tracks,
|
||||||
playerBackend: PlayerBackend,
|
|
||||||
supportsDirectPlay: Boolean,
|
|
||||||
audioIndex: Int?,
|
audioIndex: Int?,
|
||||||
subtitleIndex: Int?,
|
subtitleIndex: Int?,
|
||||||
source: MediaSourceInfo,
|
source: MediaSourceInfo,
|
||||||
): TrackSelectionResult {
|
): TrackSelectionResult {
|
||||||
val embeddedSubtitleCount = source.embeddedSubtitleCount
|
|
||||||
val externalSubtitleCount = source.externalSubtitlesCount
|
|
||||||
|
|
||||||
val paramsBuilder = trackSelectionParams.buildUpon()
|
val paramsBuilder = trackSelectionParams.buildUpon()
|
||||||
val groups = tracks.groups
|
|
||||||
|
|
||||||
val subtitleSelected =
|
val subtitleSelected =
|
||||||
if (subtitleIndex != null && subtitleIndex >= 0) {
|
if (subtitleIndex != null && subtitleIndex >= 0) {
|
||||||
val subtitleIsExternal = source.findExternalSubtitle(subtitleIndex) != null
|
val subtitleIsExternal = source.findExternalSubtitle(subtitleIndex) != null
|
||||||
if (subtitleIsExternal || supportsDirectPlay) {
|
|
||||||
val chosenTrack =
|
val chosenTrack =
|
||||||
if (subtitleIsExternal && playerBackend == PlayerBackend.EXO_PLAYER) {
|
if (subtitleIsExternal) {
|
||||||
groups.firstOrNull { group ->
|
tracks.groups.firstOrNull { group ->
|
||||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
group.type == C.TRACK_TYPE_TEXT && group.isExternal
|
||||||
(0..<group.mediaTrackGroup.length)
|
|
||||||
.mapNotNull {
|
|
||||||
group.getTrackFormat(it).id
|
|
||||||
}.any { it.endsWith("e:$subtitleIndex") }
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val actualEmbeddedCount =
|
val playerIndex =
|
||||||
groups
|
getPlayerIndex(subtitleIndex, source, MediaStreamType.SUBTITLE)
|
||||||
|
if (playerIndex != null) {
|
||||||
|
tracks.groups
|
||||||
.filter { group ->
|
.filter { group ->
|
||||||
group.type == C.TRACK_TYPE_TEXT &&
|
group.type == C.TRACK_TYPE_TEXT &&
|
||||||
(0..<group.mediaTrackGroup.length)
|
group.length >= 1 &&
|
||||||
.mapNotNull {
|
!group.isExternal
|
||||||
group.getTrackFormat(it).id
|
}
|
||||||
}.none { it.contains("e:") }
|
// TODO why are exoplayer tracks out of order sometimes?
|
||||||
}.size
|
.sortedById()
|
||||||
val indexToFind =
|
.getOrNull(playerIndex)
|
||||||
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) {
|
|
||||||
group.getTrackFormat(0).id?.contains("e:") == true
|
|
||||||
} else {
|
} else {
|
||||||
group.getTrackFormat(0).id?.contains("e:") == false
|
null
|
||||||
}
|
|
||||||
}.map {
|
|
||||||
group.getTrackFormat(it).idAsInt
|
|
||||||
}.contains(indexToFind)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
when {
|
||||||
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack")
|
chosenTrack != null && chosenTrack.isSupported -> {
|
||||||
chosenTrack?.let {
|
|
||||||
paramsBuilder
|
paramsBuilder
|
||||||
|
.clearOverridesOfType(C.TRACK_TYPE_TEXT)
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||||
.setOverrideForType(
|
.setOverrideForType(
|
||||||
TrackSelectionOverride(
|
TrackSelectionOverride(chosenTrack.mediaTrackGroup, 0),
|
||||||
chosenTrack.mediaTrackGroup,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
TrackSelected.SELECTED
|
||||||
}
|
}
|
||||||
chosenTrack != null
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
paramsBuilder
|
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
|
||||||
|
|
||||||
true
|
chosenTrack != null && !chosenTrack.isSupported -> {
|
||||||
|
TrackSelected.UNSUPPORTED
|
||||||
}
|
}
|
||||||
val audioSelected =
|
|
||||||
if (audioIndex != null && audioIndex >= 0 && supportsDirectPlay) {
|
else -> {
|
||||||
val indexToFind =
|
TrackSelected.NOT_FOUND
|
||||||
calculateIndexToFind(
|
|
||||||
audioIndex,
|
|
||||||
MediaStreamType.AUDIO,
|
|
||||||
playerBackend,
|
|
||||||
embeddedSubtitleCount,
|
|
||||||
externalSubtitleCount,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
source,
|
|
||||||
)
|
|
||||||
val chosenTrack =
|
|
||||||
groups.firstOrNull { group ->
|
|
||||||
group.type == C.TRACK_TYPE_AUDIO && group.isSupported &&
|
|
||||||
(0..<group.mediaTrackGroup.length)
|
|
||||||
.map {
|
|
||||||
group.getTrackFormat(it).idAsInt
|
|
||||||
}.contains(indexToFind)
|
|
||||||
}
|
}
|
||||||
Timber.v("Chosen audio ($audioIndex/$indexToFind) track: $chosenTrack")
|
}
|
||||||
chosenTrack?.let {
|
} else {
|
||||||
paramsBuilder
|
paramsBuilder
|
||||||
|
.clearOverridesOfType(C.TRACK_TYPE_TEXT)
|
||||||
|
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||||
|
TrackSelected.SELECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioSelected =
|
||||||
|
if (audioIndex != null && audioIndex >= 0) {
|
||||||
|
val playerIndex = getPlayerIndex(audioIndex, source, MediaStreamType.AUDIO)
|
||||||
|
val chosenTrack =
|
||||||
|
if (playerIndex != null) {
|
||||||
|
tracks.groups
|
||||||
|
.filter { group ->
|
||||||
|
group.type == C.TRACK_TYPE_AUDIO && group.length >= 1
|
||||||
|
}
|
||||||
|
// TODO why are exoplayer tracks out of order sometimes?
|
||||||
|
.sortedById()
|
||||||
|
.getOrNull(playerIndex)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
chosenTrack != null && chosenTrack.isSupported -> {
|
||||||
|
paramsBuilder
|
||||||
|
.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||||
.setOverrideForType(
|
.setOverrideForType(
|
||||||
TrackSelectionOverride(
|
TrackSelectionOverride(chosenTrack.mediaTrackGroup, 0),
|
||||||
chosenTrack.mediaTrackGroup,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
TrackSelected.SELECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
chosenTrack != null && !chosenTrack.isSupported -> {
|
||||||
|
TrackSelected.UNSUPPORTED
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
TrackSelected.NOT_FOUND
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chosenTrack != null
|
|
||||||
} else {
|
} else {
|
||||||
true
|
TrackSelected.SELECTED
|
||||||
}
|
}
|
||||||
return TrackSelectionResult(paramsBuilder.build(), audioSelected, subtitleSelected)
|
return TrackSelectionResult(paramsBuilder.build(), audioSelected, subtitleSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun getPlayerIndex(
|
||||||
* Maps the server provided index to the track index based on the [PlayerBackend] and other stream information
|
|
||||||
*/
|
|
||||||
private fun calculateIndexToFind(
|
|
||||||
serverIndex: Int,
|
serverIndex: Int,
|
||||||
type: MediaStreamType,
|
|
||||||
playerBackend: PlayerBackend,
|
|
||||||
embeddedSubtitleCount: Int,
|
|
||||||
externalSubtitleCount: Int,
|
|
||||||
subtitleIsExternal: Boolean,
|
|
||||||
actualEmbeddedCount: Int?,
|
|
||||||
source: MediaSourceInfo,
|
source: MediaSourceInfo,
|
||||||
): Int =
|
type: MediaStreamType,
|
||||||
when (playerBackend) {
|
): Int? {
|
||||||
PlayerBackend.EXO_PLAYER,
|
val playerIndex =
|
||||||
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
|
source.mediaStreams
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.indexOfFirst { it.type == MediaStreamType.AUDIO } - externalSubtitleCount
|
.filter { it.type == type }
|
||||||
serverIndex - externalSubtitleCount - videoStreamsBeforeAudioCount + 1
|
.let {
|
||||||
}
|
if (type == MediaStreamType.SUBTITLE) {
|
||||||
|
it.filter { it.deliveryMethod == SubtitleDeliveryMethod.EMBED || it.deliveryMethod == SubtitleDeliveryMethod.HLS }
|
||||||
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 {
|
} else {
|
||||||
val videoStreamCount = source.videoStreamCount
|
it
|
||||||
val audioStreamCount = source.audioStreamCount
|
|
||||||
serverIndex - externalSubtitleCount - videoStreamCount - audioStreamCount + 1
|
|
||||||
}
|
}
|
||||||
|
}.indexOfFirstOrNull { it.index == serverIndex }
|
||||||
|
return playerIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
val Tracks.Group.trackFormats: List<Format>
|
||||||
throw UnsupportedOperationException("Cannot calculate index for $type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerBackend.EXTERNAL_PLAYER -> {
|
|
||||||
throw IllegalStateException("Cannot calculate tracks external playback")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val Format.idAsInt: Int?
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
get() =
|
get() =
|
||||||
id?.let {
|
(0..<mediaTrackGroup.length)
|
||||||
if (it.contains(":")) {
|
.mapNotNull {
|
||||||
it.split(":").last().toIntOrNull()
|
getTrackFormat(it)
|
||||||
} else {
|
|
||||||
it.toIntOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val Tracks.Group.isExternal: Boolean
|
||||||
|
get() =
|
||||||
|
trackFormats.any {
|
||||||
|
it.id?.contains(":e:") == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Iterable<Tracks.Group>.sortedById(): List<Tracks.Group> =
|
||||||
|
mapNotNull { track ->
|
||||||
|
if (track.isExternal) {
|
||||||
|
// Should be filtered out before calling this though
|
||||||
|
track to listOf(Int.MAX_VALUE, Int.MAX_VALUE)
|
||||||
|
} else {
|
||||||
|
track.trackFormats[0]
|
||||||
|
.id
|
||||||
|
?.split(":")
|
||||||
|
?.map { it.toInt() }
|
||||||
|
?.let {
|
||||||
|
track to it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.sortedWith(compareBy<Pair<Tracks.Group, List<Int>>> { it.second[0] }.thenBy { it.second[1] })
|
||||||
|
.map { it.first }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -270,8 +219,18 @@ fun List<MediaStream>.findExternalSubtitle(subtitleIndex: Int?): MediaStream? =
|
||||||
*/
|
*/
|
||||||
data class TrackSelectionResult(
|
data class TrackSelectionResult(
|
||||||
val trackSelectionParameters: TrackSelectionParameters,
|
val trackSelectionParameters: TrackSelectionParameters,
|
||||||
val audioSelected: Boolean,
|
val audio: TrackSelected,
|
||||||
val subtitleSelected: Boolean,
|
val subtitle: TrackSelected,
|
||||||
) {
|
) {
|
||||||
val bothSelected: Boolean = audioSelected && subtitleSelected
|
val audioSelected: Boolean get() = audio == TrackSelected.SELECTED
|
||||||
|
|
||||||
|
val subtitleSelected: Boolean get() = subtitle == TrackSelected.SELECTED
|
||||||
|
|
||||||
|
val bothSelected: Boolean get() = audioSelected && subtitleSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TrackSelected {
|
||||||
|
SELECTED,
|
||||||
|
NOT_FOUND,
|
||||||
|
UNSUPPORTED,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import androidx.media3.common.Format
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.github.damontecres.wholphin.ui.playback.idAsInt
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
@ -159,7 +158,7 @@ fun checkForSupport(tracks: Tracks): List<TrackSupport> =
|
||||||
val reason = TrackSupportReason.fromInt(it.getTrackSupport(i))
|
val reason = TrackSupportReason.fromInt(it.getTrackSupport(i))
|
||||||
add(
|
add(
|
||||||
TrackSupport(
|
TrackSupport(
|
||||||
format.id + " (${format.idAsInt})",
|
format.id,
|
||||||
type,
|
type,
|
||||||
reason,
|
reason,
|
||||||
it.isSelected,
|
it.isSelected,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.github.damontecres.wholphin.test
|
package com.github.damontecres.wholphin.test
|
||||||
|
|
||||||
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.TrackSelectionParameters
|
import androidx.media3.common.TrackSelectionParameters
|
||||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
|
import com.github.damontecres.wholphin.ui.playback.TrackSelected
|
||||||
import com.github.damontecres.wholphin.ui.playback.TrackSelectionResult
|
import com.github.damontecres.wholphin.ui.playback.TrackSelectionResult
|
||||||
import com.github.damontecres.wholphin.ui.playback.TrackSelectionUtils
|
import com.github.damontecres.wholphin.ui.playback.TrackSelectionUtils
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
|
|
@ -36,11 +38,9 @@ class TestSelectionTrackExamples {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = testTacks.toTracks(),
|
tracks = testTacks.toTracks(),
|
||||||
playerBackend = playerBackend,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = audioIndex,
|
audioIndex = audioIndex,
|
||||||
subtitleIndex = subtitleIndex,
|
subtitleIndex = subtitleIndex,
|
||||||
source = testTacks.toMediaSourceInfo(),
|
source = builder.buildMediaSourceInfo(),
|
||||||
).also(onResult)
|
).also(onResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,4 +111,189 @@ class TestSelectionTrackExamples {
|
||||||
Assert.assertEquals("6:e:4", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
Assert.assertEquals("6:e:4", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test builderVASASS`() {
|
||||||
|
runTest(
|
||||||
|
TrackExamples.builderVASASS,
|
||||||
|
PlayerBackend.EXO_PLAYER,
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = 2,
|
||||||
|
) { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("2", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals("3", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest(
|
||||||
|
TrackExamples.builderVASASS,
|
||||||
|
PlayerBackend.MPV,
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = 2,
|
||||||
|
) { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("1:1", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals("2:1", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest(
|
||||||
|
TrackExamples.builderVASASS,
|
||||||
|
PlayerBackend.EXO_PLAYER,
|
||||||
|
audioIndex = 3,
|
||||||
|
subtitleIndex = 5,
|
||||||
|
) { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("4", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals("6", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest(
|
||||||
|
TrackExamples.builderVASASS,
|
||||||
|
PlayerBackend.MPV,
|
||||||
|
audioIndex = 3,
|
||||||
|
subtitleIndex = 5,
|
||||||
|
) { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("3:2", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals("5:3", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test external subtitles at end`() {
|
||||||
|
val builder =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addVideo()
|
||||||
|
.addAudio(2)
|
||||||
|
.addSubtitle(2)
|
||||||
|
.addExternalSubtitle(1)
|
||||||
|
|
||||||
|
runTest(
|
||||||
|
builder,
|
||||||
|
PlayerBackend.EXO_PLAYER,
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = 5, // external
|
||||||
|
) { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("0:2", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals("1:e:5", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test AAVASS in issue 1005`() {
|
||||||
|
// https://github.com/damontecres/Wholphin/issues/1005#issuecomment-4085440175
|
||||||
|
val builder =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addAudio(2)
|
||||||
|
.addVideo()
|
||||||
|
.addAudio()
|
||||||
|
.addSubtitle(42)
|
||||||
|
runTest(
|
||||||
|
builder,
|
||||||
|
PlayerBackend.MPV,
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = 5,
|
||||||
|
) { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("1:2", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals("5:2", result.trackSelectionParameters.getSubtitleOverride()?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test unsupported tracks`() {
|
||||||
|
val builder =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addVideo()
|
||||||
|
.addAudio()
|
||||||
|
.addTrack(1, MediaStreamType.AUDIO, false, C.FORMAT_UNSUPPORTED_TYPE)
|
||||||
|
|
||||||
|
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = builder.buildForExoPlayer().toTracks(),
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = builder.buildMediaSourceInfo(),
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.SELECTED, result.audio)
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = builder.buildForExoPlayer().toTracks(),
|
||||||
|
audioIndex = 2,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = builder.buildMediaSourceInfo(),
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.UNSUPPORTED, result.audio)
|
||||||
|
Assert.assertFalse(result.bothSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = builder.buildForExoPlayer().toTracks(),
|
||||||
|
audioIndex = 4,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = builder.buildMediaSourceInfo(),
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.NOT_FOUND, result.audio)
|
||||||
|
Assert.assertFalse(result.bothSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test exceeds capabilities tracks`() {
|
||||||
|
val builder =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addVideo()
|
||||||
|
.addAudio()
|
||||||
|
.addTrack(1, MediaStreamType.AUDIO, false, C.FORMAT_EXCEEDS_CAPABILITIES)
|
||||||
|
|
||||||
|
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = builder.buildForExoPlayer().toTracks(),
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = builder.buildMediaSourceInfo(),
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.SELECTED, result.audio)
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = builder.buildForExoPlayer().toTracks(),
|
||||||
|
audioIndex = 2,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = builder.buildMediaSourceInfo(),
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.UNSUPPORTED, result.audio)
|
||||||
|
Assert.assertFalse(result.bothSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = builder.buildForExoPlayer().toTracks(),
|
||||||
|
audioIndex = 4,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = builder.buildMediaSourceInfo(),
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.NOT_FOUND, result.audio)
|
||||||
|
Assert.assertFalse(result.bothSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,155 @@ class TestTrackSelection {
|
||||||
return Tracks(groups)
|
return Tracks(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 buildDifferentOrderedTracks(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 =
|
||||||
|
listOf(
|
||||||
|
Format
|
||||||
|
.Builder()
|
||||||
|
.setId("0:2")
|
||||||
|
.setSampleMimeType("audio/default")
|
||||||
|
.build(),
|
||||||
|
Format
|
||||||
|
.Builder()
|
||||||
|
.setId("0:4")
|
||||||
|
.setSampleMimeType("audio/default")
|
||||||
|
.build(),
|
||||||
|
Format
|
||||||
|
.Builder()
|
||||||
|
.setId("0:3")
|
||||||
|
.setSampleMimeType("audio/default")
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
val subtitles =
|
||||||
|
(5..8).map {
|
||||||
|
Format
|
||||||
|
.Builder()
|
||||||
|
.setId("0:$it")
|
||||||
|
.setSampleMimeType("text/default")
|
||||||
|
.build()
|
||||||
|
} +
|
||||||
|
listOf(
|
||||||
|
Format
|
||||||
|
.Builder()
|
||||||
|
.setId("1:e:8") // 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)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test MPV embedded`() {
|
fun `test MPV embedded`() {
|
||||||
val resource = javaClass.classLoader?.getResource("embedded_subs.json")
|
val resource = javaClass.classLoader?.getResource("embedded_subs.json")
|
||||||
|
|
@ -250,8 +399,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.MPV,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 1,
|
audioIndex = 1,
|
||||||
subtitleIndex = 4,
|
subtitleIndex = 4,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -265,8 +412,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.MPV,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 2,
|
audioIndex = 2,
|
||||||
subtitleIndex = 4,
|
subtitleIndex = 4,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -280,8 +425,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.MPV,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 1,
|
audioIndex = 1,
|
||||||
subtitleIndex = TrackIndex.DISABLED,
|
subtitleIndex = TrackIndex.DISABLED,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -307,8 +450,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 1,
|
audioIndex = 1,
|
||||||
subtitleIndex = 4,
|
subtitleIndex = 4,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -322,8 +463,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 2,
|
audioIndex = 2,
|
||||||
subtitleIndex = 4,
|
subtitleIndex = 4,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -337,8 +476,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 2,
|
audioIndex = 2,
|
||||||
subtitleIndex = 6,
|
subtitleIndex = 6,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -352,8 +489,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 1,
|
audioIndex = 1,
|
||||||
subtitleIndex = TrackIndex.DISABLED,
|
subtitleIndex = TrackIndex.DISABLED,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -379,8 +514,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.MPV,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 2,
|
audioIndex = 2,
|
||||||
subtitleIndex = 0,
|
subtitleIndex = 0,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -394,8 +527,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.MPV,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 3,
|
audioIndex = 3,
|
||||||
subtitleIndex = 0,
|
subtitleIndex = 0,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -421,8 +552,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 2,
|
audioIndex = 2,
|
||||||
subtitleIndex = 0,
|
subtitleIndex = 0,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -448,8 +577,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.MPV,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 3,
|
audioIndex = 3,
|
||||||
subtitleIndex = 0,
|
subtitleIndex = 0,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -465,8 +592,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.MPV,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 3,
|
audioIndex = 3,
|
||||||
subtitleIndex = 5,
|
subtitleIndex = 5,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -500,8 +625,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 3,
|
audioIndex = 3,
|
||||||
subtitleIndex = 0,
|
subtitleIndex = 0,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -523,8 +646,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 3,
|
audioIndex = 3,
|
||||||
subtitleIndex = 5,
|
subtitleIndex = 5,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -551,8 +672,6 @@ class TestTrackSelection {
|
||||||
.createTrackSelections(
|
.createTrackSelections(
|
||||||
trackSelectionParams = trackSelectionParameters,
|
trackSelectionParams = trackSelectionParameters,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
playerBackend = PlayerBackend.EXO_PLAYER,
|
|
||||||
supportsDirectPlay = true,
|
|
||||||
audioIndex = 3,
|
audioIndex = 3,
|
||||||
subtitleIndex = 2,
|
subtitleIndex = 2,
|
||||||
source = source,
|
source = source,
|
||||||
|
|
@ -570,6 +689,113 @@ 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,
|
||||||
|
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,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test ExoPlayer different order`() {
|
||||||
|
val source =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addVideo()
|
||||||
|
.addAudio(3)
|
||||||
|
.addSubtitle(4)
|
||||||
|
.addExternalSubtitle(1)
|
||||||
|
.buildMediaSourceInfo()
|
||||||
|
val tracks = buildDifferentOrderedTracks(PlayerBackend.EXO_PLAYER)
|
||||||
|
|
||||||
|
Assert.assertEquals(9, source.mediaStreams?.size)
|
||||||
|
|
||||||
|
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||||
|
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = tracks,
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = 8,
|
||||||
|
source = source,
|
||||||
|
).also { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("0:2", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals(
|
||||||
|
"1:e:8",
|
||||||
|
result.trackSelectionParameters.getSubtitleOverride()?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = tracks,
|
||||||
|
audioIndex = 3,
|
||||||
|
subtitleIndex = 8,
|
||||||
|
source = source,
|
||||||
|
).also { result ->
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
Assert.assertEquals("0:4", result.trackSelectionParameters.getAudioOverride()?.id)
|
||||||
|
Assert.assertEquals(
|
||||||
|
"1:e:8",
|
||||||
|
result.trackSelectionParameters.getSubtitleOverride()?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun TrackSelectionParameters.getAudioOverride(): Format? {
|
internal fun TrackSelectionParameters.getAudioOverride(): Format? {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import org.jellyfin.sdk.model.api.MediaSourceType
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
|
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ import org.junit.Test
|
||||||
*
|
*
|
||||||
* @see Builder
|
* @see Builder
|
||||||
*/
|
*/
|
||||||
class TestTracks private constructor(
|
class TestTracks(
|
||||||
val tracks: List<TestTrack> = emptyList(),
|
val tracks: List<TestTrack> = emptyList(),
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|
@ -27,6 +28,7 @@ class TestTracks private constructor(
|
||||||
fun toTracks(): Tracks {
|
fun toTracks(): Tracks {
|
||||||
val groups =
|
val groups =
|
||||||
tracks
|
tracks
|
||||||
|
.sortedBy { it.external }
|
||||||
.map {
|
.map {
|
||||||
val mimeType =
|
val mimeType =
|
||||||
when (it.type) {
|
when (it.type) {
|
||||||
|
|
@ -35,23 +37,176 @@ class TestTracks private constructor(
|
||||||
MediaStreamType.SUBTITLE -> "text/sample"
|
MediaStreamType.SUBTITLE -> "text/sample"
|
||||||
else -> throw UnsupportedOperationException("${it.type}")
|
else -> throw UnsupportedOperationException("${it.type}")
|
||||||
}
|
}
|
||||||
|
val format =
|
||||||
Format
|
Format
|
||||||
.Builder()
|
.Builder()
|
||||||
.setId(it.id)
|
.setId(it.id)
|
||||||
.setSampleMimeType(mimeType)
|
.setSampleMimeType(mimeType)
|
||||||
.build()
|
.build()
|
||||||
}.map { TrackGroup(it) }
|
Tracks.Group(
|
||||||
.map { Tracks.Group(it, false, intArrayOf(C.FORMAT_HANDLED), booleanArrayOf(false)) }
|
TrackGroup(format),
|
||||||
|
false,
|
||||||
|
intArrayOf(it.support),
|
||||||
|
booleanArrayOf(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return Tracks(groups)
|
return Tracks(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toMediaSourceInfo(): MediaSourceInfo {
|
/**
|
||||||
|
* Builder for adding tracks
|
||||||
|
*/
|
||||||
|
class Builder {
|
||||||
|
private val tracks = mutableListOf<TestTrackBuilder>()
|
||||||
|
|
||||||
|
fun addVideo(): Builder = addTrack(1, MediaStreamType.VIDEO, false, C.FORMAT_HANDLED)
|
||||||
|
|
||||||
|
fun addAudio(count: Int = 1): Builder = addTrack(count, MediaStreamType.AUDIO, false, C.FORMAT_HANDLED)
|
||||||
|
|
||||||
|
fun addSubtitle(count: Int = 1): Builder = addTrack(count, MediaStreamType.SUBTITLE, false, C.FORMAT_HANDLED)
|
||||||
|
|
||||||
|
fun addExternalSubtitle(count: Int = 1): Builder = addTrack(count, MediaStreamType.SUBTITLE, true, C.FORMAT_HANDLED)
|
||||||
|
|
||||||
|
fun addEmpty(count: Int = 1): Builder = addTrack(count, null, false, C.FORMAT_UNSUPPORTED_TYPE)
|
||||||
|
|
||||||
|
fun addTrack(
|
||||||
|
count: Int,
|
||||||
|
type: MediaStreamType?,
|
||||||
|
external: Boolean,
|
||||||
|
@C.FormatSupport support: Int,
|
||||||
|
): Builder {
|
||||||
|
repeat(count) {
|
||||||
|
tracks.add(TestTrackBuilder(type, external, support))
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the [TestTracks] as if being playing by ExoPlayer
|
||||||
|
*/
|
||||||
|
fun buildForExoPlayer(): TestTracks {
|
||||||
|
// Format is: [<file index>:][e:]<track index>, "e:" indicates external subtitle
|
||||||
|
val hasExternal = tracks.firstOrNull { it.external } != null
|
||||||
|
val testTracks =
|
||||||
|
buildList {
|
||||||
|
var externalSubCount = 0
|
||||||
|
val t =
|
||||||
|
tracks
|
||||||
|
.mapIndexedNotNull { index, track ->
|
||||||
|
if (track.type == null) {
|
||||||
|
return@mapIndexedNotNull null
|
||||||
|
}
|
||||||
|
if (track.external) {
|
||||||
|
externalSubCount++
|
||||||
|
TestTrack(
|
||||||
|
"$externalSubCount:e:$index",
|
||||||
|
index,
|
||||||
|
track.type,
|
||||||
|
track.external,
|
||||||
|
track.support,
|
||||||
|
)
|
||||||
|
} else if (hasExternal) {
|
||||||
|
val idx = index + 1 - externalSubCount
|
||||||
|
// If there's a sidecar file, the actual file
|
||||||
|
TestTrack(
|
||||||
|
"0:$idx",
|
||||||
|
index,
|
||||||
|
track.type,
|
||||||
|
track.external,
|
||||||
|
track.support,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val idx = index + 1
|
||||||
|
TestTrack(
|
||||||
|
"$idx",
|
||||||
|
index,
|
||||||
|
track.type,
|
||||||
|
track.external,
|
||||||
|
track.support,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addAll(t)
|
||||||
|
}
|
||||||
|
return TestTracks(testTracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the [TestTracks] as if being playing by MPV
|
||||||
|
*/
|
||||||
|
fun buildForMpv(): TestTracks {
|
||||||
|
val embeddedCount = tracks.count { !it.external }
|
||||||
|
val embeddedSubtitleCount =
|
||||||
|
tracks.count { it.type == MediaStreamType.SUBTITLE && !it.external }
|
||||||
|
val externalSubCount =
|
||||||
|
tracks.count { it.type == MediaStreamType.SUBTITLE && it.external }
|
||||||
|
// val firstNonExternal = tracks.indexOfFirst { it.type != MediaStreamType.SUBTITLE }
|
||||||
|
val firstExternal =
|
||||||
|
tracks.indexOfFirst { it.type == MediaStreamType.SUBTITLE && it.external }
|
||||||
|
val externalSubtitleAreFirst = externalSubCount > 0 && firstExternal == 0
|
||||||
|
var videoCount = 0
|
||||||
|
var audioCount = 0
|
||||||
|
var subtitleCount = 0
|
||||||
|
val testTracks =
|
||||||
|
tracks.mapIndexedNotNull { index, track ->
|
||||||
|
if (track.type == null) {
|
||||||
|
return@mapIndexedNotNull null
|
||||||
|
}
|
||||||
|
val id =
|
||||||
|
if (track.external) {
|
||||||
|
// MPV places external subtitles last
|
||||||
|
val idx = index + if (externalSubtitleAreFirst) embeddedCount else 0
|
||||||
|
subtitleCount++
|
||||||
|
val count =
|
||||||
|
subtitleCount + if (externalSubtitleAreFirst) embeddedSubtitleCount else 0
|
||||||
|
"$idx:e:$count"
|
||||||
|
} else {
|
||||||
|
val idx =
|
||||||
|
if (externalSubtitleAreFirst) index - externalSubCount else index
|
||||||
|
when (track.type) {
|
||||||
|
MediaStreamType.AUDIO -> {
|
||||||
|
audioCount++
|
||||||
|
"$idx:$audioCount"
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaStreamType.VIDEO -> {
|
||||||
|
videoCount++
|
||||||
|
"$idx:$videoCount"
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaStreamType.SUBTITLE -> {
|
||||||
|
subtitleCount++
|
||||||
|
val count =
|
||||||
|
subtitleCount - (if (externalSubtitleAreFirst) externalSubCount else 0)
|
||||||
|
"$idx:$count"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("Unsupported type " + track.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TestTrack(id, index, track.type, track.external)
|
||||||
|
}
|
||||||
|
return TestTracks(testTracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildMediaSourceInfo(): MediaSourceInfo {
|
||||||
val streams =
|
val streams =
|
||||||
tracks.mapIndexed { index, track ->
|
tracks
|
||||||
|
.filter { it.type != null }
|
||||||
|
.mapIndexed { index, track ->
|
||||||
MediaStream(
|
MediaStream(
|
||||||
index = index,
|
index = index,
|
||||||
type = track.type,
|
type = track.type!!,
|
||||||
isExternal = track.external,
|
isExternal = track.external,
|
||||||
|
deliveryMethod =
|
||||||
|
if (track.type == MediaStreamType.SUBTITLE && track.external) {
|
||||||
|
SubtitleDeliveryMethod.EXTERNAL
|
||||||
|
} else {
|
||||||
|
SubtitleDeliveryMethod.EMBED
|
||||||
|
},
|
||||||
codec = null,
|
codec = null,
|
||||||
language = null,
|
language = null,
|
||||||
isInterlaced = false,
|
isInterlaced = false,
|
||||||
|
|
@ -85,122 +240,6 @@ class TestTracks private constructor(
|
||||||
)
|
)
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builder for adding tracks
|
|
||||||
*/
|
|
||||||
class Builder {
|
|
||||||
private val tracks = mutableListOf<TestTrackBuilder>()
|
|
||||||
|
|
||||||
fun addVideo(): Builder {
|
|
||||||
tracks.add(TestTrackBuilder(MediaStreamType.VIDEO))
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addAudio(count: Int = 1): Builder {
|
|
||||||
repeat(count) {
|
|
||||||
tracks.add(TestTrackBuilder(MediaStreamType.AUDIO))
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addSubtitle(count: Int = 1): Builder {
|
|
||||||
repeat(count) {
|
|
||||||
tracks.add(TestTrackBuilder(MediaStreamType.SUBTITLE, false))
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addExternalSubtitle(count: Int = 1): Builder {
|
|
||||||
repeat(count) {
|
|
||||||
tracks.add(TestTrackBuilder(MediaStreamType.SUBTITLE, true))
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the [TestTracks] as if being playing by ExoPlayer
|
|
||||||
*/
|
|
||||||
fun buildForExoPlayer(): TestTracks {
|
|
||||||
// Format is: [<file index>:][e:]<track index>, "e:" indicates external subtitle
|
|
||||||
val hasExternal = tracks.firstOrNull { it.external } != null
|
|
||||||
val testTracks =
|
|
||||||
buildList {
|
|
||||||
var externalSubCount = 0
|
|
||||||
val t =
|
|
||||||
tracks
|
|
||||||
.mapIndexed { index, track ->
|
|
||||||
if (track.external) {
|
|
||||||
externalSubCount++
|
|
||||||
TestTrack(
|
|
||||||
"$externalSubCount:e:$index",
|
|
||||||
index,
|
|
||||||
track.type,
|
|
||||||
track.external,
|
|
||||||
)
|
|
||||||
} else if (hasExternal) {
|
|
||||||
val idx = index + 1 - externalSubCount
|
|
||||||
// If there's a sidecar file, the actual file
|
|
||||||
TestTrack("0:$idx", index, track.type, track.external)
|
|
||||||
} else {
|
|
||||||
val idx = index + 1
|
|
||||||
TestTrack("$idx", index, track.type, track.external)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addAll(t)
|
|
||||||
}
|
|
||||||
return TestTracks(testTracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the [TestTracks] as if being playing by MPV
|
|
||||||
*/
|
|
||||||
fun buildForMpv(): TestTracks {
|
|
||||||
val embeddedCount = tracks.count { !it.external }
|
|
||||||
val embeddedSubtitleCount =
|
|
||||||
tracks.count { it.type == MediaStreamType.SUBTITLE && !it.external }
|
|
||||||
val externalSubCount =
|
|
||||||
tracks.count { it.type == MediaStreamType.SUBTITLE && it.external }
|
|
||||||
var videoCount = 0
|
|
||||||
var audioCount = 0
|
|
||||||
var subtitleCount = 0
|
|
||||||
val testTracks =
|
|
||||||
tracks.mapIndexed { index, track ->
|
|
||||||
val id =
|
|
||||||
if (track.external) {
|
|
||||||
// MPV places external subtitles last
|
|
||||||
val idx = embeddedCount + index
|
|
||||||
subtitleCount++
|
|
||||||
val count = embeddedSubtitleCount + subtitleCount
|
|
||||||
"$idx:e:$count"
|
|
||||||
} else {
|
|
||||||
val idx = index - externalSubCount
|
|
||||||
when (track.type) {
|
|
||||||
MediaStreamType.AUDIO -> {
|
|
||||||
audioCount++
|
|
||||||
"$idx:$audioCount"
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaStreamType.VIDEO -> {
|
|
||||||
videoCount++
|
|
||||||
"$idx:$videoCount"
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaStreamType.SUBTITLE -> {
|
|
||||||
subtitleCount++
|
|
||||||
val count = subtitleCount - externalSubCount
|
|
||||||
"$idx:$count"
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
throw IllegalArgumentException("Unsupported type " + track.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TestTrack(id, index, track.type, track.external)
|
|
||||||
}
|
|
||||||
return TestTracks(testTracks)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -223,11 +262,13 @@ data class TestTrack(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val type: MediaStreamType,
|
val type: MediaStreamType,
|
||||||
val external: Boolean,
|
val external: Boolean,
|
||||||
|
@param:C.FormatSupport val support: Int = C.FORMAT_HANDLED,
|
||||||
)
|
)
|
||||||
|
|
||||||
class TestTrackBuilder(
|
class TestTrackBuilder(
|
||||||
val type: MediaStreamType,
|
val type: MediaStreamType?,
|
||||||
val external: Boolean = false,
|
val external: Boolean = false,
|
||||||
|
@param:C.FormatSupport val support: Int = C.FORMAT_HANDLED,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun assertIdType(
|
fun assertIdType(
|
||||||
|
|
@ -473,4 +514,80 @@ class TestTracksTests {
|
||||||
assertIdType("5:3", MediaStreamType.SUBTITLE, mpv[5])
|
assertIdType("5:3", MediaStreamType.SUBTITLE, mpv[5])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test external subtitles at end`() {
|
||||||
|
val builder =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addVideo()
|
||||||
|
.addAudio(3)
|
||||||
|
.addSubtitle(4)
|
||||||
|
.addExternalSubtitle()
|
||||||
|
|
||||||
|
builder.buildForExoPlayer().tracks.let { tracks ->
|
||||||
|
Assert.assertEquals(9, tracks.size)
|
||||||
|
Assert.assertEquals("0:1", tracks[0].id)
|
||||||
|
Assert.assertEquals("0:2", tracks[1].id)
|
||||||
|
Assert.assertEquals("0:3", tracks[2].id)
|
||||||
|
Assert.assertEquals("0:4", tracks[3].id)
|
||||||
|
Assert.assertEquals("0:5", tracks[4].id)
|
||||||
|
Assert.assertEquals("0:6", tracks[5].id)
|
||||||
|
Assert.assertEquals("0:7", tracks[6].id)
|
||||||
|
Assert.assertEquals("0:8", tracks[7].id)
|
||||||
|
Assert.assertEquals("1:e:8", tracks[8].id)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.buildForMpv().tracks.let { tracks ->
|
||||||
|
Assert.assertEquals(9, tracks.size)
|
||||||
|
Assert.assertEquals("0:1", tracks[0].id)
|
||||||
|
Assert.assertEquals("1:1", tracks[1].id)
|
||||||
|
Assert.assertEquals("2:2", tracks[2].id)
|
||||||
|
Assert.assertEquals("3:3", tracks[3].id)
|
||||||
|
Assert.assertEquals("4:1", tracks[4].id)
|
||||||
|
Assert.assertEquals("5:2", tracks[5].id)
|
||||||
|
Assert.assertEquals("6:3", tracks[6].id)
|
||||||
|
Assert.assertEquals("7:4", tracks[7].id)
|
||||||
|
Assert.assertEquals("8:e:5", tracks[8].id)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.buildMediaSourceInfo().let { source ->
|
||||||
|
val index =
|
||||||
|
source.mediaStreams?.indexOfFirst { it.type == MediaStreamType.SUBTITLE && it.isExternal }
|
||||||
|
Assert.assertEquals(8, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test AAVASS in issue 1005`() {
|
||||||
|
// https://github.com/damontecres/Wholphin/issues/1005#issuecomment-4085440175
|
||||||
|
val builder =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addAudio(2)
|
||||||
|
.addVideo()
|
||||||
|
.addAudio()
|
||||||
|
.addSubtitle(42)
|
||||||
|
|
||||||
|
builder.buildForExoPlayer().tracks.let { exo ->
|
||||||
|
Assert.assertEquals(46, exo.size)
|
||||||
|
assertIdType("1", MediaStreamType.AUDIO, exo[0])
|
||||||
|
assertIdType("2", MediaStreamType.AUDIO, exo[1])
|
||||||
|
assertIdType("3", MediaStreamType.VIDEO, exo[2])
|
||||||
|
assertIdType("4", MediaStreamType.AUDIO, exo[3])
|
||||||
|
assertIdType("5", MediaStreamType.SUBTITLE, exo[4])
|
||||||
|
assertIdType("6", MediaStreamType.SUBTITLE, exo[5])
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.buildForMpv().tracks.let { mpv ->
|
||||||
|
Assert.assertEquals(46, mpv.size)
|
||||||
|
assertIdType("0:1", MediaStreamType.AUDIO, mpv[0])
|
||||||
|
assertIdType("1:2", MediaStreamType.AUDIO, mpv[1])
|
||||||
|
assertIdType("2:1", MediaStreamType.VIDEO, mpv[2])
|
||||||
|
assertIdType("3:3", MediaStreamType.AUDIO, mpv[3])
|
||||||
|
assertIdType("4:1", MediaStreamType.SUBTITLE, mpv[4])
|
||||||
|
assertIdType("5:2", MediaStreamType.SUBTITLE, mpv[5])
|
||||||
|
assertIdType("6:3", MediaStreamType.SUBTITLE, mpv[6])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,8 +165,7 @@ class PlaybackViewModelTests {
|
||||||
.addVideo()
|
.addVideo()
|
||||||
.addAudio()
|
.addAudio()
|
||||||
.addSubtitle()
|
.addSubtitle()
|
||||||
.buildForExoPlayer()
|
.buildMediaSourceInfo()
|
||||||
.toMediaSourceInfo()
|
|
||||||
|
|
||||||
private val playSessionId = "playsessionid12345"
|
private val playSessionId = "playsessionid12345"
|
||||||
private val videoStreamUrl = "http://localhost:8096/video/stream"
|
private val videoStreamUrl = "http://localhost:8096/video/stream"
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,8 @@
|
||||||
"IsExternal": false,
|
"IsExternal": false,
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "Embed"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Codec": "ass",
|
"Codec": "ass",
|
||||||
|
|
@ -193,7 +194,8 @@
|
||||||
"IsExternal": false,
|
"IsExternal": false,
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "Embed"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Codec": "ass",
|
"Codec": "ass",
|
||||||
|
|
@ -221,7 +223,8 @@
|
||||||
"IsExternal": false,
|
"IsExternal": false,
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "Embed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"MediaAttachments": [],
|
"MediaAttachments": [],
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Path": "",
|
"Path": "",
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "External"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Codec": "subrip",
|
"Codec": "subrip",
|
||||||
|
|
@ -79,7 +80,8 @@
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Path": "",
|
"Path": "",
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "External"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Codec": "subrip",
|
"Codec": "subrip",
|
||||||
|
|
@ -107,7 +109,8 @@
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Path": "",
|
"Path": "",
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "External"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Codec": "eac3",
|
"Codec": "eac3",
|
||||||
|
|
@ -194,7 +197,8 @@
|
||||||
"IsExternal": false,
|
"IsExternal": false,
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "Embed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"MediaAttachments": [],
|
"MediaAttachments": [],
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@
|
||||||
"IsTextSubtitleStream": true,
|
"IsTextSubtitleStream": true,
|
||||||
"SupportsExternalStream": true,
|
"SupportsExternalStream": true,
|
||||||
"Path": "",
|
"Path": "",
|
||||||
"Level": 0
|
"Level": 0,
|
||||||
|
"DeliveryMethod": "External"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Codec": "hevc",
|
"Codec": "hevc",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue