Show player loading status
This commit is contained in:
parent
41ee49af72
commit
c64abead0f
11 changed files with 81 additions and 15 deletions
|
|
@ -170,6 +170,7 @@ data class PlayerSettings(
|
|||
val preferredAudioLanguage: String = AudioLanguageOption.DEVICE,
|
||||
val secondaryPreferredAudioLanguage: String? = null,
|
||||
val loadingOverlayEnabled: Boolean = true,
|
||||
val showPlayerLoadingStatus: Boolean = true,
|
||||
val pauseOverlayEnabled: Boolean = true,
|
||||
val osdClockEnabled: Boolean = true,
|
||||
val skipIntroEnabled: Boolean = true,
|
||||
|
|
@ -282,6 +283,7 @@ class PlayerSettingsDataStore @Inject constructor(
|
|||
private val preferredAudioLanguageKey = stringPreferencesKey("preferred_audio_language")
|
||||
private val secondaryPreferredAudioLanguageKey = stringPreferencesKey("secondary_preferred_audio_language")
|
||||
private val loadingOverlayEnabledKey = booleanPreferencesKey("loading_overlay_enabled")
|
||||
private val showPlayerLoadingStatusKey = booleanPreferencesKey("show_player_loading_status")
|
||||
private val pauseOverlayEnabledKey = booleanPreferencesKey("pause_overlay_enabled")
|
||||
private val osdClockEnabledKey = booleanPreferencesKey("osd_clock_enabled")
|
||||
private val skipIntroEnabledKey = booleanPreferencesKey("skip_intro_enabled")
|
||||
|
|
@ -426,6 +428,7 @@ class PlayerSettingsDataStore @Inject constructor(
|
|||
secondaryPreferredAudioLanguage = prefs[secondaryPreferredAudioLanguageKey]
|
||||
?.let(::normalizeSecondaryAudioLanguageCode),
|
||||
loadingOverlayEnabled = prefs[loadingOverlayEnabledKey] ?: true,
|
||||
showPlayerLoadingStatus = prefs[showPlayerLoadingStatusKey] ?: true,
|
||||
pauseOverlayEnabled = prefs[pauseOverlayEnabledKey] ?: true,
|
||||
osdClockEnabled = prefs[osdClockEnabledKey] ?: true,
|
||||
skipIntroEnabled = prefs[skipIntroEnabledKey] ?: true,
|
||||
|
|
@ -615,6 +618,12 @@ class PlayerSettingsDataStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun setShowPlayerLoadingStatus(enabled: Boolean) {
|
||||
store().edit { prefs ->
|
||||
prefs[showPlayerLoadingStatusKey] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setFrameRateMatchingMode(mode: FrameRateMatchingMode) {
|
||||
store().edit { prefs ->
|
||||
prefs[frameRateMatchingModeKey] = mode.name
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.inject.Inject
|
||||
|
||||
class SubtitleRepositoryImpl @Inject constructor(
|
||||
|
|
@ -33,7 +34,8 @@ class SubtitleRepositoryImpl @Inject constructor(
|
|||
videoId: String?,
|
||||
videoHash: String?,
|
||||
videoSize: Long?,
|
||||
filename: String?
|
||||
filename: String?,
|
||||
onProgress: ((completed: Int, total: Int) -> Unit)?
|
||||
): List<Subtitle> = withContext(Dispatchers.IO) {
|
||||
val requestType = canonicalSubtitleType(type)
|
||||
val startedAtMs = System.currentTimeMillis()
|
||||
|
|
@ -57,11 +59,15 @@ class SubtitleRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
Log.d(TAG, "Found ${subtitleAddons.size} subtitle addons: ${subtitleAddons.map { it.name }}")
|
||||
|
||||
|
||||
if (subtitleAddons.isEmpty()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
|
||||
|
||||
val total = subtitleAddons.size
|
||||
val completedCount = AtomicInteger(0)
|
||||
onProgress?.invoke(0, total)
|
||||
|
||||
// Fetch subtitles from all addons in parallel
|
||||
val result = coroutineScope {
|
||||
subtitleAddons.map { addon ->
|
||||
|
|
@ -70,6 +76,7 @@ class SubtitleRepositoryImpl @Inject constructor(
|
|||
val subtitles = withTimeoutOrNull(PER_ADDON_TIMEOUT_MS) {
|
||||
fetchSubtitlesFromAddon(addon, type, id, videoId, videoHash, videoSize, filename)
|
||||
}
|
||||
onProgress?.invoke(completedCount.incrementAndGet(), total)
|
||||
if (subtitles == null) {
|
||||
Log.w(
|
||||
TAG,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ interface SubtitleRepository {
|
|||
videoId: String? = null,
|
||||
videoHash: String? = null,
|
||||
videoSize: Long? = null,
|
||||
filename: String? = null
|
||||
filename: String? = null,
|
||||
onProgress: ((completed: Int, total: Int) -> Unit)? = null
|
||||
): List<Subtitle>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.nuvio.tv.ui.screens.player
|
||||
|
||||
import android.content.Context
|
||||
import com.nuvio.tv.R
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
|
|
@ -71,10 +72,12 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
|
|||
try {
|
||||
resetLoadingOverlayForNewStream()
|
||||
val playerSettings = playerSettingsDataStore.playerSettings.first()
|
||||
val showLoadingStatus = playerSettings.showPlayerLoadingStatus
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
frameRateMatchingMode = playerSettings.frameRateMatchingMode,
|
||||
resizeMode = playerSettings.resizeMode
|
||||
resizeMode = playerSettings.resizeMode,
|
||||
loadingMessage = if (showLoadingStatus) context.getString(R.string.player_loading_preparing) else null
|
||||
)
|
||||
}
|
||||
runAfrPreflightIfEnabled(
|
||||
|
|
@ -87,7 +90,7 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
|
|||
url = url,
|
||||
headers = headers
|
||||
)
|
||||
val startupSubtitlePreparation = prepareStreamStartSubtitles(playerSettings)
|
||||
val startupSubtitlePreparation = prepareStreamStartSubtitles(playerSettings, showLoadingStatus)
|
||||
requestedUseLibassByUser = playerSettings.useLibass
|
||||
val useLibass = when {
|
||||
!requestedUseLibassByUser -> false
|
||||
|
|
@ -172,6 +175,7 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
|
|||
).setExtensionRendererMode(playerSettings.decoderPriority)
|
||||
.setMapDV7ToHevc(playerSettings.mapDV7ToHevc)
|
||||
|
||||
if (showLoadingStatus) _uiState.update { it.copy(loadingMessage = context.getString(R.string.player_loading_building)) }
|
||||
val buildDefaultPlayer = {
|
||||
mediaSourceFactory.configureSubtitleParsing(
|
||||
extractorsFactory = null,
|
||||
|
|
@ -247,6 +251,7 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
|
|||
mimeTypeOverride = currentStreamMimeType
|
||||
)
|
||||
)
|
||||
if (showLoadingStatus) _uiState.update { it.copy(loadingMessage = context.getString(R.string.player_loading_starting)) }
|
||||
playWhenReady = true
|
||||
prepare()
|
||||
|
||||
|
|
@ -268,9 +273,9 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
|
|||
if (playbackState == Player.STATE_BUFFERING && !hasRenderedFirstFrame) {
|
||||
_uiState.update { state ->
|
||||
if (state.loadingOverlayEnabled && !state.showLoadingOverlay) {
|
||||
state.copy(showLoadingOverlay = true, showControls = false)
|
||||
state.copy(showLoadingOverlay = true, showControls = false, loadingMessage = if (showLoadingStatus) context.getString(R.string.player_loading_buffering) else null)
|
||||
} else {
|
||||
state
|
||||
state.copy(loadingMessage = if (showLoadingStatus) context.getString(R.string.player_loading_buffering) else null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -337,7 +342,7 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
|
|||
|
||||
override fun onRenderedFirstFrame() {
|
||||
hasRenderedFirstFrame = true
|
||||
_uiState.update { it.copy(showLoadingOverlay = false) }
|
||||
_uiState.update { it.copy(showLoadingOverlay = false, loadingMessage = null) }
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
|
|
@ -419,7 +424,8 @@ internal fun resolvePreferredAudioLanguages(
|
|||
internal suspend fun PlayerRuntimeController.prepareStartupSubtitles(
|
||||
mode: AddonSubtitleStartupMode,
|
||||
preferredLanguage: String,
|
||||
secondaryLanguage: String?
|
||||
secondaryLanguage: String?,
|
||||
showLoadingStatus: Boolean = true
|
||||
): StartupSubtitlePreparation {
|
||||
if (mode == AddonSubtitleStartupMode.FAST_STARTUP) {
|
||||
return StartupSubtitlePreparation(
|
||||
|
|
@ -460,7 +466,16 @@ internal suspend fun PlayerRuntimeController.prepareStartupSubtitles(
|
|||
_uiState.update { it.copy(isLoadingAddonSubtitles = true, addonSubtitlesError = null) }
|
||||
|
||||
val fetchedSubtitles = withTimeoutOrNull(STARTUP_SUBTITLE_PREFETCH_TIMEOUT_MS) {
|
||||
fetchAddonSubtitlesNow()
|
||||
fetchAddonSubtitlesNow(
|
||||
onProgress = if (showLoadingStatus) { completed, total ->
|
||||
val msg = if (completed == 0) {
|
||||
context.getString(R.string.player_loading_subtitles_from, total)
|
||||
} else {
|
||||
context.getString(R.string.player_loading_subtitles_progress, completed, total)
|
||||
}
|
||||
_uiState.update { it.copy(loadingMessage = msg) }
|
||||
} else null
|
||||
)
|
||||
} ?: return StartupSubtitlePreparation(
|
||||
fetchedSubtitles = emptyList(),
|
||||
attachedSubtitles = emptyList(),
|
||||
|
|
@ -503,7 +518,8 @@ internal fun PlayerRuntimeController.resetAddonSubtitleStateForNewStream() {
|
|||
}
|
||||
|
||||
internal suspend fun PlayerRuntimeController.prepareStreamStartSubtitles(
|
||||
playerSettings: PlayerSettings
|
||||
playerSettings: PlayerSettings,
|
||||
showLoadingStatus: Boolean = true
|
||||
): StartupSubtitlePreparation {
|
||||
requestedUseLibassByUser = playerSettings.useLibass
|
||||
if (libassPipelineDecisionStreamUrl != currentStreamUrl) {
|
||||
|
|
@ -516,7 +532,8 @@ internal suspend fun PlayerRuntimeController.prepareStreamStartSubtitles(
|
|||
return prepareStartupSubtitles(
|
||||
mode = playerSettings.addonSubtitleStartupMode,
|
||||
preferredLanguage = playerSettings.subtitleStyle.preferredLanguage,
|
||||
secondaryLanguage = playerSettings.subtitleStyle.secondaryPreferredLanguage
|
||||
secondaryLanguage = playerSettings.subtitleStyle.secondaryPreferredLanguage,
|
||||
showLoadingStatus = showLoadingStatus
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ internal fun PlayerRuntimeController.buildSubtitleFetchRequest(): SubtitleFetchR
|
|||
)
|
||||
}
|
||||
|
||||
internal suspend fun PlayerRuntimeController.fetchAddonSubtitlesNow(): List<Subtitle> {
|
||||
internal suspend fun PlayerRuntimeController.fetchAddonSubtitlesNow(
|
||||
onProgress: ((completed: Int, total: Int) -> Unit)? = null
|
||||
): List<Subtitle> {
|
||||
val request = buildSubtitleFetchRequest() ?: return emptyList()
|
||||
val installedAddonOrder = addonRepository.getInstalledAddons().firstOrNull()
|
||||
?.map { it.displayName }
|
||||
|
|
@ -64,7 +66,8 @@ internal suspend fun PlayerRuntimeController.fetchAddonSubtitlesNow(): List<Subt
|
|||
videoId = request.videoId,
|
||||
videoHash = currentVideoHash,
|
||||
videoSize = currentVideoSize,
|
||||
filename = currentFilename
|
||||
filename = currentFilename,
|
||||
onProgress = onProgress
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -560,6 +560,7 @@ fun PlayerScreen(
|
|||
backdropUrl = uiState.backdrop,
|
||||
logoUrl = uiState.logo,
|
||||
title = uiState.title,
|
||||
message = uiState.loadingMessage,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(2f)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ data class PlayerUiState(
|
|||
val playbackSpeed: Float = 1f,
|
||||
val loadingOverlayEnabled: Boolean = true,
|
||||
val showLoadingOverlay: Boolean = true,
|
||||
val loadingMessage: String? = null,
|
||||
val pauseOverlayEnabled: Boolean = true,
|
||||
val osdClockEnabled: Boolean = true,
|
||||
val showPauseOverlay: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ fun PlaybackSettingsContent(
|
|||
coroutineScope.launch { viewModel.setStreamAutoPlayTimeoutSeconds(seconds) }
|
||||
},
|
||||
onSetReuseLastLinkEnabled = { enabled -> coroutineScope.launch { viewModel.setStreamReuseLastLinkEnabled(enabled) } },
|
||||
onSetShowPlayerLoadingStatus = { enabled -> coroutineScope.launch { viewModel.setShowPlayerLoadingStatus(enabled) } },
|
||||
onSetLoadingOverlayEnabled = { enabled -> coroutineScope.launch { viewModel.setLoadingOverlayEnabled(enabled) } },
|
||||
onSetPauseOverlayEnabled = { enabled -> coroutineScope.launch { viewModel.setPauseOverlayEnabled(enabled) } },
|
||||
onSetOsdClockEnabled = { enabled -> coroutineScope.launch { viewModel.setOsdClockEnabled(enabled) } },
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.ChevronRight
|
|||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.PauseCircle
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Timer
|
||||
|
|
@ -113,6 +114,7 @@ internal fun PlaybackSettingsSections(
|
|||
onSetNextEpisodeThresholdMinutesBeforeEnd: (Float) -> Unit,
|
||||
onSetStreamAutoPlayTimeoutSeconds: (Int) -> Unit,
|
||||
onSetReuseLastLinkEnabled: (Boolean) -> Unit,
|
||||
onSetShowPlayerLoadingStatus: (Boolean) -> Unit,
|
||||
onSetLoadingOverlayEnabled: (Boolean) -> Unit,
|
||||
onSetPauseOverlayEnabled: (Boolean) -> Unit,
|
||||
onSetOsdClockEnabled: (Boolean) -> Unit,
|
||||
|
|
@ -319,6 +321,17 @@ internal fun PlaybackSettingsSections(
|
|||
onSetReuseLastLinkEnabled = onSetReuseLastLinkEnabled,
|
||||
onItemFocused = { focusedSection = PlaybackSection.STREAM_SELECTION }
|
||||
)
|
||||
|
||||
item(key = "stream_show_loading_status") {
|
||||
ToggleSettingsItem(
|
||||
icon = Icons.Default.Info,
|
||||
title = stringResource(R.string.playback_show_loading_status),
|
||||
subtitle = stringResource(R.string.playback_show_loading_status_sub),
|
||||
isChecked = playerSettings.showPlayerLoadingStatus,
|
||||
onCheckedChange = onSetShowPlayerLoadingStatus,
|
||||
onFocused = { focusedSection = PlaybackSection.STREAM_SELECTION }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
playbackCollapsibleSection(
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ class PlaybackSettingsViewModel @Inject constructor(
|
|||
playerSettingsDataStore.setLoadingOverlayEnabled(enabled)
|
||||
}
|
||||
|
||||
suspend fun setShowPlayerLoadingStatus(enabled: Boolean) {
|
||||
playerSettingsDataStore.setShowPlayerLoadingStatus(enabled)
|
||||
}
|
||||
|
||||
suspend fun setPauseOverlayEnabled(enabled: Boolean) {
|
||||
playerSettingsDataStore.setPauseOverlayEnabled(enabled)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -716,6 +716,15 @@
|
|||
|
||||
<!-- PlayerScreen -->
|
||||
<string name="player_no_external_player">No external player found</string>
|
||||
<string name="player_loading_preparing">Preparing stream…</string>
|
||||
<string name="player_loading_subtitles">Fetching subtitles…</string>
|
||||
<string name="player_loading_subtitles_from">Fetching subtitles from %1$d addons…</string>
|
||||
<string name="player_loading_subtitles_progress">Fetching subtitles… (%1$d / %2$d)</string>
|
||||
<string name="player_loading_building">Building player…</string>
|
||||
<string name="player_loading_starting">Starting stream…</string>
|
||||
<string name="player_loading_buffering">Buffering…</string>
|
||||
<string name="playback_show_loading_status">Show loading status</string>
|
||||
<string name="playback_show_loading_status_sub">Display step-by-step progress while the player is initializing.</string>
|
||||
|
||||
<!-- ParentalGuideOverlay -->
|
||||
<string name="parental_nudity">Nudity</string>
|
||||
|
|
|
|||
Loading…
Reference in a new issue