From 3dc1ea2b9a26f76c5e72134c302a6b56934602aa Mon Sep 17 00:00:00 2001 From: theo Date: Thu, 23 Apr 2026 21:28:36 -0300 Subject: [PATCH] Fixed overheating issues --- .../pixelplay/data/service/MusicService.kt | 6 ++- .../data/service/player/DualPlayerEngine.kt | 46 ++++++++++++++++--- .../service/player/TransitionController.kt | 7 +-- .../data/telegram/TelegramStreamProxy.kt | 24 +++++++--- .../presentation/components/LyricsSheet.kt | 6 ++- .../components/subcomps/PlayingEqIcon.kt | 7 ++- .../viewmodel/PlaybackStateHolder.kt | 6 ++- .../viewmodel/exts/DeckController.kt | 2 +- 8 files changed, 81 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index 234ae67a4..5d654377e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -180,7 +180,10 @@ class MusicService : MediaLibraryService() { const val ACTION_SLEEP_TIMER_EXPIRED = "com.theveloper.pixelplay.ACTION_SLEEP_TIMER_EXPIRED" const val EXTRA_FORCE_FOREGROUND_ON_START = "com.theveloper.pixelplay.extra.FORCE_FOREGROUND_ON_START" - private const val PLAYBACK_SNAPSHOT_DEBOUNCE_MS = 350L + // Queue/index/flags snapshot is only used for restore on process death. A full-queue + // JSON+DataStore rewrite on every Media3 event (track transition fires 3-4 listeners + // within ~200ms) is unnecessary work. 1500ms coalesces those without harming restore. + private const val PLAYBACK_SNAPSHOT_DEBOUNCE_MS = 1500L private const val FORCED_WIDGET_STATE_DEBOUNCE_MS = 90L private const val MEDIA_SESSION_BUTTON_DEBOUNCE_MS = 90L private val pendingMediaButtonForegroundStarts = AtomicInteger(0) @@ -1049,7 +1052,6 @@ class MusicService : MediaLibraryService() { } requestWidgetAndWearRefreshWithFollowUp() mediaSession?.let { refreshMediaSessionUiWithFollowUp(it) } - mediaSession?.let { refreshMediaSessionUi(it) } schedulePlaybackSnapshotPersist() } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt index 5400c89b2..1f9e0c505 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt @@ -72,6 +72,7 @@ class DualPlayerEngine @Inject constructor( private companion object { private const val AUDIO_OFFLOAD_BUFFERING_FALLBACK_MS = 4_000L private val LOCAL_MEDIA_SCHEMES = setOf("content", "file", "android.resource") + private val REMOTE_MEDIA_SCHEMES = setOf("http", "https", "telegram", "netease", "qqmusic", "navidrome", "jellyfin", "gdrive") } private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -167,7 +168,9 @@ class DualPlayerEngine @Inject constructor( // Limpieza para canciones que no son de Telegram telegramCacheManager.setActivePlayback(null) } - // Wake mode is configured once in buildPlayer(). + // Upgrade/downgrade wake policy so local playback does not keep the radio awake + // and cloud/remote playback still holds the network wake lock. + applyWakeModeForCurrentItem() // --- Pre-Resolve Next/Prev Tracks para Performance --- try { @@ -337,6 +340,33 @@ class DualPlayerEngine @Inject constructor( return scheme == null || scheme in LOCAL_MEDIA_SCHEMES } + /** + * Selects the cheapest wake policy that still keeps playback reliable. + * Network wake is only needed when the current queue item actually talks to the network — + * local file playback keeps only CPU awake so the device can sleep the radio. + */ + private fun wakeModeFor(mediaItem: MediaItem?): Int { + val scheme = mediaItem?.localConfiguration?.uri?.scheme?.lowercase() + return if (scheme != null && scheme in REMOTE_MEDIA_SCHEMES) { + C.WAKE_MODE_NETWORK + } else { + C.WAKE_MODE_LOCAL + } + } + + private fun applyWakeModeForCurrentItem() { + if (!::playerA.isInitialized) return + val mode = wakeModeFor(playerA.currentMediaItem) + try { + playerA.setWakeMode(mode) + if (::playerB.isInitialized) { + playerB.setWakeMode(mode) + } + } catch (e: Exception) { + Timber.tag("DualPlayerEngine").w(e, "Failed to update wake mode") + } + } + private fun shouldDisableAudioOffloadByDefault(): Boolean { val manufacturer = Build.MANUFACTURER.lowercase() val brand = Build.BRAND.lowercase() @@ -388,6 +418,7 @@ class DualPlayerEngine @Inject constructor( playerA.shuffleModeEnabled = shuffleMode playerA.prepare() playerA.playWhenReady = desiredPlayWhenReady + applyWakeModeForCurrentItem() } _activeAudioSessionId.value = playerA.audioSessionId @@ -510,10 +541,11 @@ class DualPlayerEngine @Inject constructor( .build() ) setHandleAudioBecomingNoisy(true) // Force player to pause automatically when audio is rerouted from a headset to device speakers - // Cloud sources are proxied through localhost, but the proxy still depends on - // upstream network access. Keep both CPU and network awake so background - // playback does not stall when the screen turns off or the app is backgrounded. - setWakeMode(C.WAKE_MODE_NETWORK) + // Default to CPU-only wake. Upgraded to WAKE_MODE_NETWORK dynamically when the + // current item is a remote/cloud source via applyWakeModeForCurrentItem(). + // Keeping local playback on WAKE_MODE_LOCAL lets the radio sleep, which is one + // of the biggest heat savings for long sessions on weaker phones. + setWakeMode(C.WAKE_MODE_LOCAL) // Explicitly keep both players live so they can overlap without affecting each other playWhenReady = false Timber.tag("DualPlayerEngine").d("Built player with audio offload %s", if (audioOffloadEnabled) "enabled" else "disabled") @@ -933,7 +965,9 @@ class DualPlayerEngine @Inject constructor( // playerB is now the Outgoing/Aux. val duration = settings.durationMs.toLong().coerceAtLeast(500L) - val stepMs = 16L + // 30Hz volume ramp is indistinguishable from 60Hz at the AudioTrack frame boundary + // for a multi-second crossfade; halves wake-ups during overlap. + val stepMs = 32L var elapsed = 0L var lastLog = 0L diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt index cb6f050ed..c5e01b1f7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt @@ -250,13 +250,14 @@ class TransitionController @Inject constructor( return@collectLatest } - // Wait loop with adaptive sleep + // Wait loop with adaptive sleep. 250ms near-end cadence still lands the crossfade + // within ±125ms of the target — imperceptible for a multi-second overlap, and 5× + // fewer wakeups in the last second of every track. while (player.currentPosition < transitionPoint && isActive) { val remaining = transitionPoint - player.currentPosition val sleep = when { remaining > 5000 -> 1000L - remaining > 1000 -> 250L - else -> 50L // Tight loop near the end + else -> 250L } if (remaining < 2000 && remaining % 500 < 50) { Timber.tag("TransitionDebug").v("Countdown: %d ms to transition", remaining) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/telegram/TelegramStreamProxy.kt b/app/src/main/java/com/theveloper/pixelplay/data/telegram/TelegramStreamProxy.kt index 6350c710f..cb0867b26 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/telegram/TelegramStreamProxy.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/telegram/TelegramStreamProxy.kt @@ -195,9 +195,16 @@ class TelegramStreamProxy @Inject constructor( var currentPos = start val buffer = ByteArray(64 * 1024) // Increased to 64KB for smoother streaming var noDataCount = 0 - + // Exponential backoff while the reader is waiting for TDLib to + // deliver more bytes. The previous fixed 50ms delay combined with + // a per-iteration getFile() call kept the IO thread and TDLib + // database churning during any stall, which showed up as sustained + // CPU heat on weaker devices during cloud playback. + var stallDelayMs = 50L + val maxStallDelayMs = 400L + raf.seek(currentPos) - + var cachedDownloadedPrefixSize = fileInfo?.local?.downloadedPrefixSize?.toLong() ?: 0L while (true) { @@ -218,14 +225,19 @@ class TelegramStreamProxy @Inject constructor( // If size is different than expected, we still stop because we can't get more. break } - + // Verify cancellation/failure if (updatedInfo?.local?.isDownloadingCompleted == false && !updatedInfo.local.canBeDownloaded) { break // Failed/Cancelled } - - delay(50) // Wait for more data + + delay(stallDelayMs) + stallDelayMs = (stallDelayMs * 2).coerceAtMost(maxStallDelayMs) continue + } else { + // New data arrived — reset the backoff so we stay + // responsive once the download catches up. + stallDelayMs = 50L } } @@ -233,7 +245,7 @@ class TelegramStreamProxy @Inject constructor( // Read min of: buffer size, remaining in range, remaining valid bytes val remainingValid = cachedDownloadedPrefixSize - currentPos val toRead = min(buffer.size.toLong(), min(remaining, remainingValid)).toInt() - + val read = raf.read(buffer, 0, toRead) if (read > 0) { writeFully(buffer, 0, read) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt index 836ad3cf5..08023daf7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt @@ -1769,11 +1769,13 @@ private fun LyricsTrackInfo( LaunchedEffect(isPlaying) { if (isPlaying) { - // Spin forever + // Spin forever. 8s per revolution halves the effective per-second animation work + // vs the original 4s cadence — visually still clearly a rotating "vinyl", but + // drives fewer Compose invalidations during long listening sessions. while (true) { currentRotation.animateTo( targetValue = currentRotation.value + 360f, - animationSpec = tween(4000, easing = LinearEasing) + animationSpec = tween(8000, easing = LinearEasing) ) } } else { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/PlayingEqIcon.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/PlayingEqIcon.kt index 6ed9743f3..47a32afe9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/PlayingEqIcon.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/PlayingEqIcon.kt @@ -27,8 +27,11 @@ fun PlayingEqIcon( bars: Int = 3, minHeightFraction: Float = 0.28f, maxHeightFraction: Float = 1.0f, - phaseDurationMillis: Int = 2400, // ciclo más lento - wanderDurationMillis: Int = 8000, // patrón más largo + // Slower cycles mean fewer animation frames / Canvas redraws per second while the icon + // is visible. With many current-song indicators potentially on screen (home, queue, + // lyrics sheet), this noticeably lowers screen-on CPU on weaker devices. + phaseDurationMillis: Int = 3600, // ciclo más lento + wanderDurationMillis: Int = 12000, // patrón más largo gapFraction: Float = 0.30f ) { val fullRotation = (2f * PI).toFloat() diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt index ca7913b7c..17cb10b09 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt @@ -42,7 +42,11 @@ class PlaybackStateHolder @Inject constructor( companion object { private const val TAG = "PlaybackStateHolder" private const val DURATION_MISMATCH_TOLERANCE_MS = 1500L - private const val FOREGROUND_PROGRESS_TICK_MS = 250L + // 500 ms keeps the progress bar/time display smooth to the eye (it still updates twice + // per second) while halving Compose recomposition pressure vs. the old 250 ms tick. + // The visual slider uses frame-clock interpolation on top of this, so the animation + // stays fluid even at a slower underlying cadence. + private const val FOREGROUND_PROGRESS_TICK_MS = 500L private const val BACKGROUND_PROGRESS_TICK_MS = 1000L /** * Threshold above which we skip per-item moveMediaItem calls and use diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/exts/DeckController.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/exts/DeckController.kt index ab78cea37..2cb1dbf71 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/exts/DeckController.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/exts/DeckController.kt @@ -73,7 +73,7 @@ class DeckController( .build() ) setHandleAudioBecomingNoisy(true) - setWakeMode(C.WAKE_MODE_NETWORK) + setWakeMode(C.WAKE_MODE_LOCAL) } }