Fix live tv & recorded tv playback (#1770)
## Description #1654 broke playback for both live TV and recorded programs in `.ts` format. This PR fixes both issues! Jellyfin returns `NoCompatibleStream` for live tv if the source ID is passed in, so that is now skipped for live tv. `.ts` files may have IDs formatted with a slash instead of colon, so the logic is updated to handle that plus fallbacks for potentially unknown future formats. ### Related issues Fixes #1767 ### Testing Emulator & nvidia shield, plus unit tests ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
7c6ab39436
commit
6ad96865e7
4 changed files with 116 additions and 33 deletions
|
|
@ -466,10 +466,16 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val mediaSource = streamChoiceService.chooseSource(base, itemPlayback)
|
val mediaSource =
|
||||||
|
if (!isLiveTv) {
|
||||||
|
streamChoiceService.chooseSource(base, itemPlayback)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
val plc = streamChoiceService.getPlaybackLanguageChoice(base)
|
val plc = streamChoiceService.getPlaybackLanguageChoice(base)
|
||||||
|
|
||||||
if (mediaSource == null) {
|
if (mediaSource == null && !isLiveTv) {
|
||||||
showToast(
|
showToast(
|
||||||
context,
|
context,
|
||||||
"Item has no media sources, skipping...",
|
"Item has no media sources, skipping...",
|
||||||
|
|
@ -479,7 +485,8 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
val videoStream =
|
val videoStream =
|
||||||
mediaSource.mediaStreams
|
mediaSource
|
||||||
|
?.mediaStreams
|
||||||
?.firstOrNull { it.type == MediaStreamType.VIDEO }
|
?.firstOrNull { it.type == MediaStreamType.VIDEO }
|
||||||
?.let {
|
?.let {
|
||||||
val isHdr =
|
val isHdr =
|
||||||
|
|
@ -493,37 +500,48 @@ class PlaybackViewModel
|
||||||
// Create the correct player for the media
|
// Create the correct player for the media
|
||||||
createPlayer(videoStream?.hdr == true, videoStream?.is4k == true)
|
createPlayer(videoStream?.hdr == true, videoStream?.is4k == true)
|
||||||
|
|
||||||
val subtitleStreams = getSubtitleStreams(mediaSource)
|
val subtitleStreams = mediaSource?.let { getSubtitleStreams(mediaSource) }.orEmpty()
|
||||||
val audioStreams = getAudioStreams(mediaSource)
|
val audioStreams = mediaSource?.let { getAudioStreams(mediaSource) }.orEmpty()
|
||||||
val audioStream =
|
val audioStream =
|
||||||
streamChoiceService
|
mediaSource?.let {
|
||||||
.chooseAudioStream(
|
streamChoiceService
|
||||||
source = mediaSource,
|
.chooseAudioStream(
|
||||||
seriesId = base.seriesId,
|
source = mediaSource,
|
||||||
itemPlayback = itemPlayback,
|
seriesId = base.seriesId,
|
||||||
plc = plc,
|
itemPlayback = itemPlayback,
|
||||||
prefs = preferences,
|
plc = plc,
|
||||||
)
|
prefs = preferences,
|
||||||
|
)
|
||||||
|
}
|
||||||
val audioIndex = audioStream?.index
|
val audioIndex = audioStream?.index
|
||||||
|
|
||||||
val subtitleIndex =
|
val subtitleIndex =
|
||||||
streamChoiceService
|
mediaSource?.let {
|
||||||
.chooseSubtitleStream(
|
streamChoiceService
|
||||||
source = mediaSource,
|
.chooseSubtitleStream(
|
||||||
audioStream = audioStream,
|
source = mediaSource,
|
||||||
seriesId = base.seriesId,
|
audioStream = audioStream,
|
||||||
itemPlayback = itemPlayback,
|
seriesId = base.seriesId,
|
||||||
plc = plc,
|
itemPlayback = itemPlayback,
|
||||||
prefs = preferences,
|
plc = plc,
|
||||||
)?.index
|
prefs = preferences,
|
||||||
|
)?.index
|
||||||
Timber.d("Selected mediaSource=${mediaSource.id}, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
}
|
||||||
|
Timber.d(
|
||||||
|
"Selected mediaSource=%s, audioIndex=%s, subtitleIndex=%s",
|
||||||
|
mediaSource?.id,
|
||||||
|
audioIndex,
|
||||||
|
subtitleIndex,
|
||||||
|
)
|
||||||
|
|
||||||
val trickPlayInfo =
|
val trickPlayInfo =
|
||||||
item.data.trickplay
|
mediaSource?.id?.let { sourceId ->
|
||||||
?.get(mediaSource.id)
|
item.data.trickplay
|
||||||
?.values
|
?.get(sourceId)
|
||||||
?.firstOrNull()
|
?.values
|
||||||
|
?.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
trickPlayInfo?.let { trickplayInfo ->
|
trickPlayInfo?.let { trickplayInfo ->
|
||||||
mediaSource.runTimeTicks?.ticks?.let { duration ->
|
mediaSource.runTimeTicks?.ticks?.let { duration ->
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
|
@ -545,7 +563,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
updateCurrentMedia {
|
updateCurrentMedia {
|
||||||
CurrentMediaInfo(
|
CurrentMediaInfo(
|
||||||
sourceId = mediaSource.id,
|
sourceId = mediaSource?.id,
|
||||||
videoStream = videoStream,
|
videoStream = videoStream,
|
||||||
audioStreams = audioStreams,
|
audioStreams = audioStreams,
|
||||||
subtitleStreams = subtitleStreams,
|
subtitleStreams = subtitleStreams,
|
||||||
|
|
@ -559,7 +577,7 @@ class PlaybackViewModel
|
||||||
audioIndex = audioIndex,
|
audioIndex = audioIndex,
|
||||||
subtitleIndex = subtitleIndex,
|
subtitleIndex = subtitleIndex,
|
||||||
positionMs = if (positionMs > 0) positionMs else C.TIME_UNSET,
|
positionMs = if (positionMs > 0) positionMs else C.TIME_UNSET,
|
||||||
sourceId = mediaSource.id,
|
sourceId = mediaSource?.id,
|
||||||
enableDirectPlay = !forceTranscoding,
|
enableDirectPlay = !forceTranscoding,
|
||||||
enableDirectStream = !forceTranscoding,
|
enableDirectStream = !forceTranscoding,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -158,10 +158,16 @@ object TrackSelectionUtils {
|
||||||
} else {
|
} else {
|
||||||
track.trackFormats[0]
|
track.trackFormats[0]
|
||||||
.id
|
.id
|
||||||
?.split(":")
|
?.split(":", "/")
|
||||||
?.map { it.toInt() }
|
?.map { it.toIntOrNull() ?: Int.MAX_VALUE }
|
||||||
?.let {
|
?.let {
|
||||||
track to it
|
if (it.size >= 2) {
|
||||||
|
track to it
|
||||||
|
} else if (it.isEmpty()) {
|
||||||
|
track to listOf(Int.MAX_VALUE, Int.MAX_VALUE)
|
||||||
|
} else {
|
||||||
|
track to listOf(it[0], Int.MAX_VALUE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.sortedWith(compareBy<Pair<Tracks.Group, List<Int>>> { it.second[0] }.thenBy { it.second[1] })
|
}.sortedWith(compareBy<Pair<Tracks.Group, List<Int>>> { it.second[0] }.thenBy { it.second[1] })
|
||||||
|
|
|
||||||
|
|
@ -296,4 +296,61 @@ class TestSelectionTrackExamples {
|
||||||
Assert.assertFalse(result.bothSelected)
|
Assert.assertFalse(result.bothSelected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test exo track IDs with slashes`() {
|
||||||
|
val testTracks =
|
||||||
|
TestTracks(
|
||||||
|
listOf(
|
||||||
|
// .ts files (eg recorded TV) report IDs like this in ExoPlayer
|
||||||
|
TestTrack("3/49", 0, MediaStreamType.VIDEO, false),
|
||||||
|
TestTrack("3/52", 1, MediaStreamType.AUDIO, false),
|
||||||
|
TestTrack("", 2, MediaStreamType.DATA, false),
|
||||||
|
TestTrack("3/53", 3, MediaStreamType.AUDIO, false),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val mediaSource =
|
||||||
|
TestTracks
|
||||||
|
.Builder()
|
||||||
|
.addVideo()
|
||||||
|
.addAudio()
|
||||||
|
.addTrack(1, MediaStreamType.DATA, false, C.FORMAT_HANDLED)
|
||||||
|
.addAudio()
|
||||||
|
.buildMediaSourceInfo()
|
||||||
|
|
||||||
|
val trackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = testTracks.toTracks(),
|
||||||
|
audioIndex = 1,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = mediaSource,
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.SELECTED, result.audio)
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
}
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = testTracks.toTracks(),
|
||||||
|
audioIndex = 2,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = mediaSource,
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.NOT_FOUND, result.audio)
|
||||||
|
Assert.assertFalse(result.bothSelected)
|
||||||
|
}
|
||||||
|
TrackSelectionUtils
|
||||||
|
.createTrackSelections(
|
||||||
|
trackSelectionParams = trackSelectionParameters,
|
||||||
|
tracks = testTracks.toTracks(),
|
||||||
|
audioIndex = 3,
|
||||||
|
subtitleIndex = null,
|
||||||
|
source = mediaSource,
|
||||||
|
).let { result ->
|
||||||
|
Assert.assertEquals(TrackSelected.SELECTED, result.audio)
|
||||||
|
Assert.assertTrue(result.bothSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.test
|
||||||
|
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
import androidx.media3.common.TrackGroup
|
import androidx.media3.common.TrackGroup
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
import org.jellyfin.sdk.model.api.MediaProtocol
|
import org.jellyfin.sdk.model.api.MediaProtocol
|
||||||
|
|
@ -35,6 +36,7 @@ class TestTracks(
|
||||||
MediaStreamType.VIDEO -> "video/sample"
|
MediaStreamType.VIDEO -> "video/sample"
|
||||||
MediaStreamType.AUDIO -> "audio/default"
|
MediaStreamType.AUDIO -> "audio/default"
|
||||||
MediaStreamType.SUBTITLE -> "text/sample"
|
MediaStreamType.SUBTITLE -> "text/sample"
|
||||||
|
MediaStreamType.DATA -> MimeTypes.APPLICATION_ID3
|
||||||
else -> throw UnsupportedOperationException("${it.type}")
|
else -> throw UnsupportedOperationException("${it.type}")
|
||||||
}
|
}
|
||||||
val format =
|
val format =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue