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)
|
||||
|
||||
if (mediaSource == null) {
|
||||
if (mediaSource == null && !isLiveTv) {
|
||||
showToast(
|
||||
context,
|
||||
"Item has no media sources, skipping...",
|
||||
|
|
@ -479,7 +485,8 @@ class PlaybackViewModel
|
|||
}
|
||||
|
||||
val videoStream =
|
||||
mediaSource.mediaStreams
|
||||
mediaSource
|
||||
?.mediaStreams
|
||||
?.firstOrNull { it.type == MediaStreamType.VIDEO }
|
||||
?.let {
|
||||
val isHdr =
|
||||
|
|
@ -493,37 +500,48 @@ class PlaybackViewModel
|
|||
// Create the correct player for the media
|
||||
createPlayer(videoStream?.hdr == true, videoStream?.is4k == true)
|
||||
|
||||
val subtitleStreams = getSubtitleStreams(mediaSource)
|
||||
val audioStreams = getAudioStreams(mediaSource)
|
||||
val subtitleStreams = mediaSource?.let { getSubtitleStreams(mediaSource) }.orEmpty()
|
||||
val audioStreams = mediaSource?.let { getAudioStreams(mediaSource) }.orEmpty()
|
||||
val audioStream =
|
||||
streamChoiceService
|
||||
.chooseAudioStream(
|
||||
source = mediaSource,
|
||||
seriesId = base.seriesId,
|
||||
itemPlayback = itemPlayback,
|
||||
plc = plc,
|
||||
prefs = preferences,
|
||||
)
|
||||
mediaSource?.let {
|
||||
streamChoiceService
|
||||
.chooseAudioStream(
|
||||
source = mediaSource,
|
||||
seriesId = base.seriesId,
|
||||
itemPlayback = itemPlayback,
|
||||
plc = plc,
|
||||
prefs = preferences,
|
||||
)
|
||||
}
|
||||
val audioIndex = audioStream?.index
|
||||
|
||||
val subtitleIndex =
|
||||
streamChoiceService
|
||||
.chooseSubtitleStream(
|
||||
source = mediaSource,
|
||||
audioStream = audioStream,
|
||||
seriesId = base.seriesId,
|
||||
itemPlayback = itemPlayback,
|
||||
plc = plc,
|
||||
prefs = preferences,
|
||||
)?.index
|
||||
|
||||
Timber.d("Selected mediaSource=${mediaSource.id}, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||
mediaSource?.let {
|
||||
streamChoiceService
|
||||
.chooseSubtitleStream(
|
||||
source = mediaSource,
|
||||
audioStream = audioStream,
|
||||
seriesId = base.seriesId,
|
||||
itemPlayback = itemPlayback,
|
||||
plc = plc,
|
||||
prefs = preferences,
|
||||
)?.index
|
||||
}
|
||||
Timber.d(
|
||||
"Selected mediaSource=%s, audioIndex=%s, subtitleIndex=%s",
|
||||
mediaSource?.id,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
)
|
||||
|
||||
val trickPlayInfo =
|
||||
item.data.trickplay
|
||||
?.get(mediaSource.id)
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
mediaSource?.id?.let { sourceId ->
|
||||
item.data.trickplay
|
||||
?.get(sourceId)
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
}
|
||||
|
||||
trickPlayInfo?.let { trickplayInfo ->
|
||||
mediaSource.runTimeTicks?.ticks?.let { duration ->
|
||||
viewModelScope.launchIO {
|
||||
|
|
@ -545,7 +563,7 @@ class PlaybackViewModel
|
|||
}
|
||||
updateCurrentMedia {
|
||||
CurrentMediaInfo(
|
||||
sourceId = mediaSource.id,
|
||||
sourceId = mediaSource?.id,
|
||||
videoStream = videoStream,
|
||||
audioStreams = audioStreams,
|
||||
subtitleStreams = subtitleStreams,
|
||||
|
|
@ -559,7 +577,7 @@ class PlaybackViewModel
|
|||
audioIndex = audioIndex,
|
||||
subtitleIndex = subtitleIndex,
|
||||
positionMs = if (positionMs > 0) positionMs else C.TIME_UNSET,
|
||||
sourceId = mediaSource.id,
|
||||
sourceId = mediaSource?.id,
|
||||
enableDirectPlay = !forceTranscoding,
|
||||
enableDirectStream = !forceTranscoding,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -158,10 +158,16 @@ object TrackSelectionUtils {
|
|||
} else {
|
||||
track.trackFormats[0]
|
||||
.id
|
||||
?.split(":")
|
||||
?.map { it.toInt() }
|
||||
?.split(":", "/")
|
||||
?.map { it.toIntOrNull() ?: Int.MAX_VALUE }
|
||||
?.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] })
|
||||
|
|
|
|||
|
|
@ -296,4 +296,61 @@ class TestSelectionTrackExamples {
|
|||
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.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.TrackGroup
|
||||
import androidx.media3.common.Tracks
|
||||
import org.jellyfin.sdk.model.api.MediaProtocol
|
||||
|
|
@ -35,6 +36,7 @@ class TestTracks(
|
|||
MediaStreamType.VIDEO -> "video/sample"
|
||||
MediaStreamType.AUDIO -> "audio/default"
|
||||
MediaStreamType.SUBTITLE -> "text/sample"
|
||||
MediaStreamType.DATA -> MimeTypes.APPLICATION_ID3
|
||||
else -> throw UnsupportedOperationException("${it.type}")
|
||||
}
|
||||
val format =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue