mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Show/change subtitles & change audio tracks
This commit is contained in:
parent
f3df36e1a1
commit
9cc2fc40d3
4 changed files with 166 additions and 13 deletions
|
|
@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
|
|
@ -33,9 +34,15 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.text.Cue
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.compose.PlayerSurface
|
||||
import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW
|
||||
import androidx.media3.ui.compose.modifiers.resizeWithContentScale
|
||||
|
|
@ -45,6 +52,7 @@ import androidx.media3.ui.compose.state.rememberPresentationState
|
|||
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
|
|
@ -62,6 +70,7 @@ fun PlaybackContent(
|
|||
modifier: Modifier = Modifier,
|
||||
viewModel: PlaybackViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(destination.itemId) {
|
||||
viewModel.init(destination, deviceProfile, preferences)
|
||||
|
|
@ -76,6 +85,26 @@ fun PlaybackContent(
|
|||
val subtitleStreams by viewModel.subtitleStreams.observeAsState(listOf())
|
||||
val trickplay by viewModel.trickplay.observeAsState(null)
|
||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||
val currentPlayback by viewModel.currentPlayback.observeAsState(null)
|
||||
|
||||
var cues by remember { mutableStateOf<List<Cue>>(listOf()) }
|
||||
|
||||
// TODO move to viewmodel?
|
||||
val cueListener =
|
||||
remember {
|
||||
object : Player.Listener {
|
||||
override fun onCues(cueGroup: CueGroup) {
|
||||
cues = cueGroup.cues
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OneTimeLaunchedEffect {
|
||||
player.addListener(cueListener)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { player.removeListener(cueListener) }
|
||||
}
|
||||
|
||||
if (stream == null) {
|
||||
// TODO loading
|
||||
|
|
@ -234,14 +263,35 @@ fun PlaybackContent(
|
|||
scale = contentScale,
|
||||
playbackSpeed = playbackSpeed,
|
||||
moreButtonOptions = MoreButtonOptions(mapOf()),
|
||||
subtitleIndex = null,
|
||||
audioIndex = null,
|
||||
subtitleIndex = currentPlayback?.subtitleIndex,
|
||||
audioIndex = currentPlayback?.audioIndex,
|
||||
audioStreams = audioStreams,
|
||||
trickplayInfo = trickplay,
|
||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
|
||||
if (!controllerViewState.controlsVisible && skipIndicatorDuration == 0L && currentPlayback?.subtitleIndex != null) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
SubtitleView(context).apply {
|
||||
setUserDefaultStyle()
|
||||
setUserDefaultTextSize()
|
||||
}
|
||||
},
|
||||
update = {
|
||||
it.setCues(cues)
|
||||
},
|
||||
onReset = {
|
||||
it.setCues(null)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Transparent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -58,6 +57,7 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.R
|
||||
import com.github.damontecres.dolphin.ui.AppColors
|
||||
import com.github.damontecres.dolphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.dolphin.ui.seekBack
|
||||
import com.github.damontecres.dolphin.ui.seekForward
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
|
|
@ -349,18 +349,25 @@ fun RightPlaybackButtons(
|
|||
)
|
||||
}
|
||||
if (showCaptionDialog) {
|
||||
val context = LocalContext.current
|
||||
val options = subtitleStreams.map { it.displayName }
|
||||
Timber.v("subtitleIndex=$subtitleIndex, options=$options")
|
||||
val currentChoice =
|
||||
subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex }?.plus(1) ?: 0
|
||||
BottomDialog(
|
||||
choices = options,
|
||||
currentChoice = subtitleIndex,
|
||||
choices = listOf("None") + options,
|
||||
currentChoice = currentChoice,
|
||||
onDismissRequest = {
|
||||
onControllerInteraction.invoke()
|
||||
showCaptionDialog = false
|
||||
},
|
||||
onSelectChoice = { index, _ ->
|
||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(subtitleStreams[index].index))
|
||||
val send =
|
||||
if (index == 0) {
|
||||
-1
|
||||
} else {
|
||||
subtitleStreams[index - 1].index
|
||||
}
|
||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(send))
|
||||
},
|
||||
gravity = Gravity.END,
|
||||
)
|
||||
|
|
@ -387,7 +394,7 @@ fun RightPlaybackButtons(
|
|||
if (showAudioDialog) {
|
||||
BottomDialog(
|
||||
choices = audioStreams.map { it.displayName },
|
||||
currentChoice = audioIndex,
|
||||
currentChoice = audioStreams.indexOfFirstOrNull { it.index == audioIndex },
|
||||
onDismissRequest = {
|
||||
onControllerInteraction.invoke()
|
||||
showAudioDialog = false
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
package com.github.damontecres.dolphin.ui.playback
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.github.damontecres.dolphin.data.model.Chapter
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
|
|
@ -27,6 +33,7 @@ import org.jellyfin.sdk.api.client.extensions.videosApi
|
|||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
|
|
@ -157,6 +164,7 @@ class PlaybackViewModel
|
|||
audioStreams.firstOrNull { it.language == "eng" }?.index
|
||||
?: audioStreams.firstOrNull()?.index
|
||||
|
||||
Timber.v("base.mediaStreams=${base.mediaStreams}")
|
||||
Timber.v("subtitleTracks=$subtitleStreams")
|
||||
Timber.v("audioStreams=$audioStreams")
|
||||
Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||
|
|
@ -183,6 +191,7 @@ class PlaybackViewModel
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun changeStreams(
|
||||
itemId: UUID,
|
||||
audioIndex: Int?,
|
||||
|
|
@ -256,6 +265,24 @@ class PlaybackViewModel
|
|||
decision.mediaItem,
|
||||
positionMs,
|
||||
)
|
||||
if (audioIndex != null || subtitleIndex != null) {
|
||||
val trackActivationListener =
|
||||
object : Player.Listener {
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
Timber.v("onTracksChanged: $tracks")
|
||||
if (tracks.groups.isNotEmpty()) {
|
||||
applyTrackSelections(
|
||||
player,
|
||||
source,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
)
|
||||
player.removeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
player.addListener(trackActivationListener)
|
||||
}
|
||||
}
|
||||
val trickPlayInfo =
|
||||
dto.trickplay
|
||||
|
|
@ -312,3 +339,77 @@ data class CurrentPlayback(
|
|||
val subtitleIndex: Int?,
|
||||
val mediaSourceId: UUID?,
|
||||
)
|
||||
|
||||
private val Format.idAsInt: Int?
|
||||
@OptIn(UnstableApi::class)
|
||||
get() =
|
||||
id?.let {
|
||||
if (it.contains(":")) {
|
||||
it.split(":").last().toIntOrNull()
|
||||
} else {
|
||||
it.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun applyTrackSelections(
|
||||
player: Player,
|
||||
source: MediaSourceInfo,
|
||||
audioIndex: Int?,
|
||||
subtitleIndex: Int?,
|
||||
) {
|
||||
if (source.supportsDirectPlay) {
|
||||
if (subtitleIndex != null && subtitleIndex >= 0) {
|
||||
val chosenTrack =
|
||||
player.currentTracks.groups.firstOrNull { group ->
|
||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
||||
(0..<group.mediaTrackGroup.length)
|
||||
.map {
|
||||
group.getTrackFormat(it).idAsInt
|
||||
}.contains(subtitleIndex + 1)
|
||||
}
|
||||
Timber.v("Chosen subtitle track: $chosenTrack")
|
||||
chosenTrack?.let {
|
||||
player.trackSelectionParameters =
|
||||
player.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||
.setOverrideForType(
|
||||
TrackSelectionOverride(
|
||||
chosenTrack.mediaTrackGroup,
|
||||
0,
|
||||
),
|
||||
).build()
|
||||
}
|
||||
} else {
|
||||
player.trackSelectionParameters =
|
||||
player.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||
.build()
|
||||
}
|
||||
if (audioIndex != null) {
|
||||
val chosenTrack =
|
||||
player.currentTracks.groups.firstOrNull { group ->
|
||||
group.type == C.TRACK_TYPE_AUDIO && group.isSupported &&
|
||||
(0..<group.mediaTrackGroup.length)
|
||||
.map {
|
||||
group.getTrackFormat(it).idAsInt
|
||||
}.contains(audioIndex + 1)
|
||||
}
|
||||
Timber.v("Chosen audio track: $chosenTrack")
|
||||
chosenTrack?.let {
|
||||
player.trackSelectionParameters =
|
||||
player.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||
.setOverrideForType(
|
||||
TrackSelectionOverride(
|
||||
chosenTrack.mediaTrackGroup,
|
||||
0,
|
||||
),
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@ class PlaybackListener : Player.Listener {
|
|||
var currentPlaylistIndex = -2
|
||||
|
||||
override fun onCues(cueGroup: CueGroup) {
|
||||
// val cues =
|
||||
// cueGroup.cues
|
||||
// .mapNotNull { it.text }
|
||||
// .joinToString("\n")
|
||||
// Log.v(TAG, "onCues: \n$cues")
|
||||
val subtitles = cueGroup.cues.ifEmpty { null }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue