vc=21: seamless background-audio handoff on 🎧 + HOME
vc=20 fixed channel videos but left two player rough edges that Cobb called out on the phone: * Tapping 🎧 (background audio) restarted the stream from the beginning instead of picking up where the foreground player was. * Pressing HOME on the player auto-entered Picture-in-Picture; what Cobb wants is seamless background audio. Both paths now share one handoff: capture exoPlayer.currentPosition, stop the activity player, start PlaybackService with EXTRA_POSITION_MS, and seekTo(position) on setMediaItem. Verified on emulator: 🎧 tap from ~34s in-track resumes service at position=34821ms. HOME-on-player triggers the same path via StrawActivity.onUserLeaveHint → PlayerLeaveHandler.handler (a tiny registry the active PlayerScreen registers in a DisposableEffect; cleared on dispose so the hook is a no-op anywhere else in the app). The previous DisposableEffect that called setAutoEnterEnabled(true) is gone; manual PiP via the ⊟ overlay button stays — that one is still useful and user-triggered. Also fixes a latent IllegalStateException that the new HOME path exposed: PlaybackService and PlayerScreen both built MediaSession with the default empty ID, which the system rejects when both live in the same process. Service now sets .setId("straw-bg") so the two sessions can coexist during the brief activity-vs-service overlap during handoff. Smoke (emulator 1440x3040): * Subscriptions feed renders (vc=20 fix carries over). * 🎧 from ~34s in NCS / Different Heaven → service playing position=34821ms, no session-ID crash. * HOME from PlayerScreen → focus moves to NexusLauncher, PlaybackService running with isForeground=true, pictureInPictureParams=null on the activity (no PiP).
This commit is contained in:
parent
709af57f42
commit
599d299b2a
5 changed files with 70 additions and 24 deletions
|
|
@ -16,6 +16,14 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app"
|
|||
|
||||
// Sulkta fork — Straw
|
||||
//
|
||||
// vc=21 / 0.1.0-AG — player hand-off polish:
|
||||
// * 🎧 background-audio button now captures the current position and
|
||||
// resumes the foreground service from there instead of restarting.
|
||||
// * HOME / recents button while on the player now hands off seamlessly
|
||||
// to background audio (same position-preserving path) instead of
|
||||
// auto-entering Picture-in-Picture. Manual PiP via the ⊟ overlay
|
||||
// button is unchanged.
|
||||
//
|
||||
// vc=20 / 0.1.0-AF — channel-videos fix on top of the rust pipeline
|
||||
// cutover. vc=19 returned empty subscription feeds because
|
||||
// strawcore-core's channel_info wasn't doing the second browse for the
|
||||
|
|
@ -24,6 +32,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app"
|
|||
// vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via
|
||||
// strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no
|
||||
// NewPipeExtractor in the runtime path.
|
||||
const val STRAW_VERSION_CODE = 20
|
||||
const val STRAW_VERSION_NAME = "0.1.0-AF"
|
||||
const val STRAW_VERSION_CODE = 21
|
||||
const val STRAW_VERSION_NAME = "0.1.0-AG"
|
||||
const val STRAW_APPLICATION_ID = "com.sulkta.straw"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.DisposableEffect
|
|||
import androidx.compose.ui.Modifier
|
||||
import com.sulkta.straw.feature.channel.ChannelScreen
|
||||
import com.sulkta.straw.feature.detail.VideoDetailScreen
|
||||
import com.sulkta.straw.feature.player.PlayerLeaveHandler
|
||||
import com.sulkta.straw.feature.player.PlayerScreen
|
||||
import com.sulkta.straw.feature.search.SearchScreen
|
||||
import com.sulkta.straw.feature.settings.SettingsScreen
|
||||
|
|
@ -109,6 +110,16 @@ class StrawActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HOME / recents → seamless hand-off to background audio when the
|
||||
* player screen is active. PlayerScreen registers the handler; any
|
||||
* other screen leaves it null and home behaves normally.
|
||||
*/
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
PlayerLeaveHandler.handler?.invoke()
|
||||
}
|
||||
|
||||
/** Pull a YouTube URL out of an incoming Intent (VIEW or SEND). */
|
||||
private fun pickYouTubeUrl(intent: Intent?): String? {
|
||||
intent ?: return null
|
||||
|
|
|
|||
|
|
@ -101,7 +101,12 @@ class PlaybackService : MediaSessionService() {
|
|||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
|
||||
// Distinct session ID so we don't collide with the activity-side
|
||||
// MediaSession (also in this process) when the user hands off from
|
||||
// PlayerScreen → background audio. Default ID is "" which throws
|
||||
// IllegalStateException("Session ID must be unique. ID=").
|
||||
mediaSession = MediaSession.Builder(this, player)
|
||||
.setId(MEDIA_SESSION_ID)
|
||||
.setSessionActivity(sessionActivityIntent)
|
||||
.build()
|
||||
}
|
||||
|
|
@ -122,6 +127,7 @@ class PlaybackService : MediaSessionService() {
|
|||
val url = intent?.getStringExtra(EXTRA_URL)?.takeIf { isAllowedAudioUrl(it) }
|
||||
val title = intent?.getStringExtra(EXTRA_TITLE)
|
||||
val uploader = intent?.getStringExtra(EXTRA_UPLOADER)
|
||||
val startPositionMs = intent?.getLongExtra(EXTRA_POSITION_MS, 0L)?.coerceAtLeast(0L) ?: 0L
|
||||
val player = mediaSession?.player
|
||||
if (url == null || player == null) {
|
||||
// HIGH-2: nothing to play (likely a re-launch with null intent
|
||||
|
|
@ -139,7 +145,7 @@ class PlaybackService : MediaSessionService() {
|
|||
.build(),
|
||||
)
|
||||
.build()
|
||||
player.setMediaItem(item)
|
||||
player.setMediaItem(item, startPositionMs)
|
||||
player.prepare()
|
||||
player.playWhenReady = true
|
||||
return START_NOT_STICKY
|
||||
|
|
@ -231,8 +237,10 @@ class PlaybackService : MediaSessionService() {
|
|||
const val EXTRA_URL = "com.sulkta.straw.extra.URL"
|
||||
const val EXTRA_TITLE = "com.sulkta.straw.extra.TITLE"
|
||||
const val EXTRA_UPLOADER = "com.sulkta.straw.extra.UPLOADER"
|
||||
const val EXTRA_POSITION_MS = "com.sulkta.straw.extra.POSITION_MS"
|
||||
|
||||
private const val NOTIF_CHANNEL_ID = "straw.playback"
|
||||
private const val NOTIF_ID = 4242
|
||||
private const val MEDIA_SESSION_ID = "straw-bg"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Sulkta-Coop
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Bridge between StrawActivity.onUserLeaveHint() (HOME / recents button)
|
||||
* and the active PlayerScreen. When the user leaves the player by pressing
|
||||
* HOME, we want a seamless hand-off to the background-audio foreground
|
||||
* service — not Picture-in-Picture. PlayerScreen registers a handler on
|
||||
* compose, clears it on dispose; the activity calls it from the OS hook.
|
||||
*/
|
||||
|
||||
package com.sulkta.straw.feature.player
|
||||
|
||||
object PlayerLeaveHandler {
|
||||
@Volatile
|
||||
var handler: (() -> Unit)? = null
|
||||
}
|
||||
|
|
@ -135,29 +135,29 @@ fun PlayerScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// PiP setup: on Android 12+ tell the OS this activity can auto-enter
|
||||
// PiP, so when the user presses Home or swipes away the video shrinks
|
||||
// into a floating window instead of pausing/exiting. Aspect ratio is
|
||||
// set eagerly so the system can sample it before the first transition.
|
||||
val activity = context as? Activity
|
||||
DisposableEffect(activity) {
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(16, 9))
|
||||
.setAutoEnterEnabled(true)
|
||||
.build()
|
||||
runCatching { activity.setPictureInPictureParams(params) }
|
||||
}
|
||||
onDispose {
|
||||
// Disable auto-enter when leaving the player so the rest of the
|
||||
// app doesn't accidentally PiP on background.
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val off = PictureInPictureParams.Builder()
|
||||
.setAutoEnterEnabled(false)
|
||||
.build()
|
||||
runCatching { activity.setPictureInPictureParams(off) }
|
||||
// Home / recents button → seamless hand-off to background audio service
|
||||
// instead of Picture-in-Picture. PiP is still available manually via the
|
||||
// ⊟ overlay button. We register a handler that the activity calls from
|
||||
// onUserLeaveHint(); the handler captures currentPosition so the audio
|
||||
// service resumes from the same point. Same code path that the explicit
|
||||
// 🎧 button uses.
|
||||
val resolvedState = androidx.compose.runtime.rememberUpdatedState(state.resolved)
|
||||
DisposableEffect(Unit) {
|
||||
PlayerLeaveHandler.handler = handler@{
|
||||
val r = resolvedState.value ?: return@handler
|
||||
val audio = r.audioUrl ?: r.combinedUrl ?: return@handler
|
||||
val position = exoPlayer.currentPosition.coerceAtLeast(0L)
|
||||
runCatching { exoPlayer.stop() }
|
||||
runCatching { exoPlayer.clearMediaItems() }
|
||||
val intent = Intent(context, PlaybackService::class.java).apply {
|
||||
component = ComponentName(context, PlaybackService::class.java)
|
||||
putExtra(PlaybackService.EXTRA_URL, audio)
|
||||
putExtra(PlaybackService.EXTRA_TITLE, title)
|
||||
putExtra(PlaybackService.EXTRA_POSITION_MS, position)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
onDispose { PlayerLeaveHandler.handler = null }
|
||||
}
|
||||
|
||||
// AUD-MED: pause playback when app goes to background. Without this,
|
||||
|
|
@ -382,12 +382,14 @@ fun PlayerScreen(
|
|||
Toast.makeText(context, "no audio stream", Toast.LENGTH_SHORT).show()
|
||||
return@OverlayButton
|
||||
}
|
||||
val position = exoPlayer.currentPosition.coerceAtLeast(0L)
|
||||
runCatching { exoPlayer.stop() }
|
||||
runCatching { exoPlayer.clearMediaItems() }
|
||||
val intent = Intent(context, PlaybackService::class.java).apply {
|
||||
component = ComponentName(context, PlaybackService::class.java)
|
||||
putExtra(PlaybackService.EXTRA_URL, audio)
|
||||
putExtra(PlaybackService.EXTRA_TITLE, title)
|
||||
putExtra(PlaybackService.EXTRA_POSITION_MS, position)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
Toast.makeText(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue