Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ef6180776 | ||
|
|
878ac5fa59 |
@@ -6,6 +6,23 @@ Entries are grouped by the capability they change, not by commit. Requirement
|
|||||||
IDs in parentheses point at [docs/requirements.md](docs/requirements.md); the
|
IDs in parentheses point at [docs/requirements.md](docs/requirements.md); the
|
||||||
generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
|
generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
|
||||||
|
|
||||||
|
## v0.4.1
|
||||||
|
|
||||||
|
### 🐛 Fixes
|
||||||
|
|
||||||
|
- **The lockscreen pause works while a video's audio plays in the background.**
|
||||||
|
The handoff starts native audio and only then tears the WebView `<video>`
|
||||||
|
down — and that teardown fires a DOM `pause` the frontend reports like any
|
||||||
|
other, which left the controller believing webview media was still the
|
||||||
|
player. Transport stayed aimed at it: pressing pause on the lockscreen sent a
|
||||||
|
control command to a `<video>` that no longer existed while the native player
|
||||||
|
carried on, and the element's parting position report dragged the displayed
|
||||||
|
time backwards. A handoff is now tracked explicitly, so it hands transport to
|
||||||
|
the native backend and ignores what the dying element still reports. A pause
|
||||||
|
made from the lockscreen also survives the return to the app, instead of being
|
||||||
|
undone by the play state captured when the handoff began.
|
||||||
|
(UR-040, UR-005 → DR-052, DR-097)
|
||||||
|
|
||||||
## v0.4.0
|
## v0.4.0
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jellytau",
|
"name": "jellytau",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1994,7 +1994,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jellytau"
|
name = "jellytau"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "jellytau"
|
name = "jellytau"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -714,7 +714,7 @@ pub async fn player_enter_background_audio(
|
|||||||
// this base to the native player's relative position to get the absolute one.
|
// this base to the native player's relative position to get the absolute one.
|
||||||
// The controller owns it so a backend-driven advance to the next episode
|
// The controller owns it so a backend-driven advance to the next episode
|
||||||
// clears it along with the stream it described.
|
// clears it along with the stream it described.
|
||||||
controller.set_background_audio_base(position_seconds);
|
controller.enter_background_audio(position_seconds);
|
||||||
controller
|
controller
|
||||||
.play_item(media_item)
|
.play_item(media_item)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@@ -753,7 +753,7 @@ pub async fn player_exit_background_audio(
|
|||||||
// The base offset (handoff position) + native player's relative position =
|
// The base offset (handoff position) + native player's relative position =
|
||||||
// the absolute position to resume the video at. Zero after a backend-driven
|
// the absolute position to resume the video at. Zero after a backend-driven
|
||||||
// episode advance, whose stream already starts at its own zero.
|
// episode advance, whose stream already starts at its own zero.
|
||||||
let base = controller.take_background_audio_base();
|
let base = controller.exit_background_audio();
|
||||||
// Capture position into a `let` BEFORE stop() — never hold work across a lock
|
// Capture position into a `let` BEFORE stop() — never hold work across a lock
|
||||||
// re-entrant call (deadlock discipline, CLAUDE.md).
|
// re-entrant call (deadlock discipline, CLAUDE.md).
|
||||||
let relative = controller.position();
|
let relative = controller.position();
|
||||||
|
|||||||
@@ -179,6 +179,17 @@ pub struct PlayerController {
|
|||||||
// TRACES: UR-040 | DR-052
|
// TRACES: UR-040 | DR-052
|
||||||
background_audio_base: Arc<Mutex<f64>>,
|
background_audio_base: Arc<Mutex<f64>>,
|
||||||
|
|
||||||
|
// True while a background-audio handoff owns playback: the native audio
|
||||||
|
// player is the real player and the webview <video> has been torn down.
|
||||||
|
//
|
||||||
|
// The teardown is what makes this necessary. It fires a DOM `pause` that the
|
||||||
|
// frontend reports like any other, which would otherwise leave the controller
|
||||||
|
// believing webview media is still active — aiming lockscreen transport at an
|
||||||
|
// element that no longer exists (see `is_html5_active`).
|
||||||
|
//
|
||||||
|
// TRACES: UR-040 | DR-052, DR-097
|
||||||
|
background_audio_active: Arc<Mutex<bool>>,
|
||||||
|
|
||||||
// Budget for re-opening a stream that ended short of the item's runtime.
|
// Budget for re-opening a stream that ended short of the item's runtime.
|
||||||
//
|
//
|
||||||
// A resume re-requests the same URL, so a server that is genuinely gone would
|
// A resume re-requests the same URL, so a server that is genuinely gone would
|
||||||
@@ -221,6 +232,7 @@ impl PlayerController {
|
|||||||
end_reason: Arc::new(Mutex::new(None)),
|
end_reason: Arc::new(Mutex::new(None)),
|
||||||
autoplay_episode_count: Arc::new(Mutex::new(0)),
|
autoplay_episode_count: Arc::new(Mutex::new(0)),
|
||||||
background_audio_base: Arc::new(Mutex::new(0.0)),
|
background_audio_base: Arc::new(Mutex::new(0.0)),
|
||||||
|
background_audio_active: Arc::new(Mutex::new(false)),
|
||||||
stream_resume: Arc::new(Mutex::new(stream_end::ResumeTracker::default())),
|
stream_resume: Arc::new(Mutex::new(stream_end::ResumeTracker::default())),
|
||||||
html5_playing: Arc::new(Mutex::new(None)),
|
html5_playing: Arc::new(Mutex::new(None)),
|
||||||
};
|
};
|
||||||
@@ -1000,6 +1012,14 @@ impl PlayerController {
|
|||||||
/// Re-emits a `StateChanged` event identical to what MpvBackend/ExoPlayer
|
/// Re-emits a `StateChanged` event identical to what MpvBackend/ExoPlayer
|
||||||
/// would emit, so `playerEvents.ts` needs no HTML5-specific branch.
|
/// would emit, so `playerEvents.ts` needs no HTML5-specific branch.
|
||||||
pub fn report_html5_state(&self, state: String, media_id: Option<String>) {
|
pub fn report_html5_state(&self, state: String, media_id: Option<String>) {
|
||||||
|
// A background-audio handoff has already moved playback to the native
|
||||||
|
// player and torn the element down; anything it still reports describes
|
||||||
|
// a video that is no longer playing. Dropping it keeps the UI on the
|
||||||
|
// audio that IS playing and leaves transport with the native backend.
|
||||||
|
if self.is_background_audio_active() {
|
||||||
|
debug!("[PlayerController] Ignoring HTML5 state '{state}' during background audio");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Track it: this is the authoritative play/pause state for
|
// Track it: this is the authoritative play/pause state for
|
||||||
// webview-rendered media, and what transport decisions read (DR-097).
|
// webview-rendered media, and what transport decisions read (DR-097).
|
||||||
// "stopped"/"idle" mean the element is gone, so hand authority back to
|
// "stopped"/"idle" mean the element is gone, so hand authority back to
|
||||||
@@ -1027,6 +1047,11 @@ impl PlayerController {
|
|||||||
/// Re-emits a `PositionUpdate` event mirroring the native backends' periodic
|
/// Re-emits a `PositionUpdate` event mirroring the native backends' periodic
|
||||||
/// position updates (the adapter is expected to throttle to ~250ms like MPV).
|
/// position updates (the adapter is expected to throttle to ~250ms like MPV).
|
||||||
pub fn report_html5_position(&self, position: f64, duration: f64) {
|
pub fn report_html5_position(&self, position: f64, duration: f64) {
|
||||||
|
// Stale by definition during a handoff — the native player's ticks are
|
||||||
|
// the real position. See `report_html5_state`.
|
||||||
|
if self.is_background_audio_active() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||||
emitter.emit(PlayerStatusEvent::PositionUpdate { position, duration });
|
emitter.emit(PlayerStatusEvent::PositionUpdate { position, duration });
|
||||||
}
|
}
|
||||||
@@ -1035,6 +1060,10 @@ impl PlayerController {
|
|||||||
/// Report that the HTML5 <video> element finished loading and knows its
|
/// Report that the HTML5 <video> element finished loading and knows its
|
||||||
/// duration. Mirrors the native `MediaLoaded` event.
|
/// duration. Mirrors the native `MediaLoaded` event.
|
||||||
pub fn report_html5_media_loaded(&self, duration: f64) {
|
pub fn report_html5_media_loaded(&self, duration: f64) {
|
||||||
|
// See `report_html5_state` — the element is not the player right now.
|
||||||
|
if self.is_background_audio_active() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||||
emitter.emit(PlayerStatusEvent::MediaLoaded { duration });
|
emitter.emit(PlayerStatusEvent::MediaLoaded { duration });
|
||||||
}
|
}
|
||||||
@@ -1234,6 +1263,42 @@ impl PlayerController {
|
|||||||
*self.background_audio_base.lock_safe() = seconds.max(0.0);
|
*self.background_audio_base.lock_safe() = seconds.max(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enter a background-audio handoff at `position` (the video's position, and
|
||||||
|
/// therefore the audio stream's zero).
|
||||||
|
///
|
||||||
|
/// Hands transport authority to the native audio player: the webview
|
||||||
|
/// `<video>` is about to be torn down, so its last reports — including the
|
||||||
|
/// `pause` the teardown itself fires — must not keep it looking like the
|
||||||
|
/// player. Without this the lockscreen pause emitted a ControlCommand at a
|
||||||
|
/// dead element and the audio played straight through it.
|
||||||
|
///
|
||||||
|
/// TRACES: UR-040, UR-005 | DR-052, DR-097
|
||||||
|
pub fn enter_background_audio(&self, position: f64) {
|
||||||
|
self.set_background_audio_base(position);
|
||||||
|
*self.background_audio_active.lock_safe() = true;
|
||||||
|
*self.html5_playing.lock_safe() = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Leave a background-audio handoff, returning the base offset to add to the
|
||||||
|
/// native player's relative position.
|
||||||
|
///
|
||||||
|
/// The webview `<video>` becomes the player again once it reloads, so its
|
||||||
|
/// reports are honoured from here on.
|
||||||
|
///
|
||||||
|
/// TRACES: UR-040, UR-005 | DR-052, DR-097
|
||||||
|
pub fn exit_background_audio(&self) -> f64 {
|
||||||
|
*self.background_audio_active.lock_safe() = false;
|
||||||
|
self.take_background_audio_base()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True while the native audio player owns playback via a background-audio
|
||||||
|
/// handoff.
|
||||||
|
///
|
||||||
|
/// TRACES: UR-040 | DR-052
|
||||||
|
pub fn is_background_audio_active(&self) -> bool {
|
||||||
|
*self.background_audio_active.lock_safe()
|
||||||
|
}
|
||||||
|
|
||||||
/// Read and clear the background-audio base offset.
|
/// Read and clear the background-audio base offset.
|
||||||
///
|
///
|
||||||
/// TRACES: UR-040 | DR-052
|
/// TRACES: UR-040 | DR-052
|
||||||
@@ -2002,6 +2067,84 @@ mod tests {
|
|||||||
assert_eq!(controls, vec!["play".to_string(), "pause".to_string()]);
|
assert_eq!(controls, vec!["play".to_string(), "pause".to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_background_audio_handoff_moves_transport_to_native_backend() {
|
||||||
|
// Lockscreen pause while playing a video's audio in the background.
|
||||||
|
//
|
||||||
|
// The handoff tears the WebView <video> down AFTER native audio starts,
|
||||||
|
// and that teardown fires a DOM `pause` the frontend dutifully reports.
|
||||||
|
// That report used to leave `html5_playing = Some(false)`, so transport
|
||||||
|
// kept being aimed at an element that no longer exists: the lockscreen
|
||||||
|
// pause emitted a ControlCommand into the void and the audio played on.
|
||||||
|
let controller = PlayerController::default();
|
||||||
|
let emitter = Arc::new(CapturingEmitter::new());
|
||||||
|
controller.set_event_emitter(emitter.clone());
|
||||||
|
|
||||||
|
// Video was playing in the webview.
|
||||||
|
controller.report_html5_state("playing".to_string(), Some("ep-1".to_string()));
|
||||||
|
assert!(controller.is_html5_active());
|
||||||
|
|
||||||
|
// Hand off to the native audio player, then tear the element down.
|
||||||
|
controller.enter_background_audio(1200.0);
|
||||||
|
controller.report_html5_state("paused".to_string(), Some("ep-1".to_string()));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!controller.is_html5_active(),
|
||||||
|
"native audio owns transport during a background-audio handoff"
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.pause().unwrap();
|
||||||
|
let controls: Vec<_> = emitter
|
||||||
|
.events()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| match e {
|
||||||
|
PlayerStatusEvent::ControlCommand { action, .. } => Some(action),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
controls.is_empty(),
|
||||||
|
"pause must drive the native backend, not a torn-down element: {:?}",
|
||||||
|
controls
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_background_audio_handoff_suppresses_stale_element_events() {
|
||||||
|
// The dying element's pause/position reports describe the video, not the
|
||||||
|
// audio now playing — re-emitting them flips the UI to paused and yanks
|
||||||
|
// the position backwards while native audio keeps going.
|
||||||
|
let controller = PlayerController::default();
|
||||||
|
let emitter = Arc::new(CapturingEmitter::new());
|
||||||
|
controller.set_event_emitter(emitter.clone());
|
||||||
|
|
||||||
|
controller.enter_background_audio(1200.0);
|
||||||
|
controller.report_html5_state("paused".to_string(), Some("ep-1".to_string()));
|
||||||
|
controller.report_html5_position(1200.0, 2400.0);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
emitter.events().is_empty(),
|
||||||
|
"stale webview reports must not reach the event pipeline: {:?}",
|
||||||
|
emitter.events()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exit_background_audio_returns_transport_to_the_webview() {
|
||||||
|
// Back in the foreground the <video> is the player again, so its reports
|
||||||
|
// must be honoured — and the base offset still comes back for the resume.
|
||||||
|
let controller = PlayerController::default();
|
||||||
|
let emitter = Arc::new(CapturingEmitter::new());
|
||||||
|
controller.set_event_emitter(emitter.clone());
|
||||||
|
|
||||||
|
controller.enter_background_audio(1200.0);
|
||||||
|
assert_eq!(controller.exit_background_audio(), 1200.0);
|
||||||
|
|
||||||
|
controller.report_html5_state("playing".to_string(), Some("ep-1".to_string()));
|
||||||
|
assert!(controller.is_html5_active());
|
||||||
|
assert!(controller.html5_is_playing());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_html5_stopped_report_releases_transport_to_native_backend() {
|
fn test_html5_stopped_report_releases_transport_to_native_backend() {
|
||||||
// When webview video goes away, transport must fall back to the native
|
// When webview video goes away, transport must fall back to the native
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "jellytau",
|
"productName": "jellytau",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"identifier": "com.dtourolle.jellytau",
|
"identifier": "com.dtourolle.jellytau",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026, UR-040, UR-061 | DR-010, DR-023, DR-024, DR-051, DR-052, DR-092, DR-098, DR-099 -->
|
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026, UR-040, UR-061 | DR-010, DR-023, DR-024, DR-051, DR-052, DR-092, DR-098, DR-099 -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy, untrack } from "svelte";
|
import { onMount, onDestroy, untrack } from "svelte";
|
||||||
|
import { get } from "svelte/store";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import type { JRayActor } from "$lib/api/bindings";
|
import type { JRayActor } from "$lib/api/bindings";
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
import CachedImage from "../common/CachedImage.svelte";
|
import CachedImage from "../common/CachedImage.svelte";
|
||||||
import { videoFitClass } from "./videoFit";
|
import { videoFitClass } from "./videoFit";
|
||||||
import { sleepTimerActive, sleepTimerExpiredSignal } from "$lib/stores/sleepTimer";
|
import { sleepTimerActive, sleepTimerExpiredSignal } from "$lib/stores/sleepTimer";
|
||||||
import { playbackPosition } from "$lib/stores/player";
|
import { playbackPosition, playerState } from "$lib/stores/player";
|
||||||
import * as html5Adapter from "$lib/player/html5Adapter";
|
import * as html5Adapter from "$lib/player/html5Adapter";
|
||||||
import { playerController } from "$lib/player";
|
import { playerController } from "$lib/player";
|
||||||
import { Html5PlayerAdapter, type Html5ElementBridge } from "$lib/player/adapters";
|
import { Html5PlayerAdapter, type Html5ElementBridge } from "$lib/player/adapters";
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
initialHandoffState,
|
initialHandoffState,
|
||||||
shouldEnterBackgroundAudio,
|
shouldEnterBackgroundAudio,
|
||||||
shouldExitBackgroundAudio,
|
shouldExitBackgroundAudio,
|
||||||
|
shouldResumeOnForeground,
|
||||||
type BackgroundAudioState,
|
type BackgroundAudioState,
|
||||||
} from "./backgroundAudioHandoff";
|
} from "./backgroundAudioHandoff";
|
||||||
|
|
||||||
@@ -1333,7 +1335,11 @@
|
|||||||
// the position native reached, and restore play/pause.
|
// the position native reached, and restore play/pause.
|
||||||
async function exitBackgroundAudioHandoff() {
|
async function exitBackgroundAudioHandoff() {
|
||||||
if (!shouldExitBackgroundAudio(handoffState)) return;
|
if (!shouldExitBackgroundAudio(handoffState)) return;
|
||||||
const wasPlaying = handoffState.wasPlaying;
|
// Read the native player's state BEFORE exiting — the exit stops it. If the
|
||||||
|
// user hit pause on the lockscreen while backgrounded, that pause must
|
||||||
|
// survive the return to video rather than being overwritten by whatever the
|
||||||
|
// <video> was doing when we handed off.
|
||||||
|
const wasPlaying = shouldResumeOnForeground(handoffState.wasPlaying, get(playerState).kind);
|
||||||
handoffState = { ...initialHandoffState };
|
handoffState = { ...initialHandoffState };
|
||||||
try {
|
try {
|
||||||
// Absolute position the native audio reached (base offset applied in Rust).
|
// Absolute position the native audio reached (base offset applied in Rust).
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
initialHandoffState,
|
initialHandoffState,
|
||||||
shouldEnterBackgroundAudio,
|
shouldEnterBackgroundAudio,
|
||||||
shouldExitBackgroundAudio,
|
shouldExitBackgroundAudio,
|
||||||
|
shouldResumeOnForeground,
|
||||||
type BackgroundAudioState,
|
type BackgroundAudioState,
|
||||||
} from "./backgroundAudioHandoff";
|
} from "./backgroundAudioHandoff";
|
||||||
|
|
||||||
@@ -58,4 +59,27 @@ describe("backgroundAudioHandoff", () => {
|
|||||||
expect(shouldExitBackgroundAudio(active)).toBe(true);
|
expect(shouldExitBackgroundAudio(active)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shouldResumeOnForeground", () => {
|
||||||
|
it("resumes when it was playing and the native audio still is", () => {
|
||||||
|
expect(shouldResumeOnForeground(true, "playing")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays paused when the lockscreen paused the native audio", () => {
|
||||||
|
// The whole point of the lockscreen pause: coming back to the app must not
|
||||||
|
// undo it just because the video was playing when we handed off.
|
||||||
|
expect(shouldResumeOnForeground(true, "paused")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays paused when the video was already paused at handoff", () => {
|
||||||
|
expect(shouldResumeOnForeground(false, "playing")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the captured state when native state is unknown", () => {
|
||||||
|
// Loading/seeking/idle say nothing about intent — the handoff snapshot is
|
||||||
|
// the best evidence we have, so a playing video still resumes.
|
||||||
|
expect(shouldResumeOnForeground(true, "loading")).toBe(true);
|
||||||
|
expect(shouldResumeOnForeground(true, undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,3 +55,22 @@ export function shouldEnterBackgroundAudio(
|
|||||||
export function shouldExitBackgroundAudio(state: BackgroundAudioState): boolean {
|
export function shouldExitBackgroundAudio(state: BackgroundAudioState): boolean {
|
||||||
return state.active;
|
return state.active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the `<video>` should start playing again once it reloads on foreground.
|
||||||
|
*
|
||||||
|
* `wasPlaying` is what the video was doing when we handed off, but the native
|
||||||
|
* audio player kept going after that — and the lockscreen/notification can pause
|
||||||
|
* it while backgrounded. The player is the authoritative source of play/pause,
|
||||||
|
* so an explicit `paused` from it overrides the handoff snapshot; anything less
|
||||||
|
* definite (loading, seeking, already-stopped, no state at all) falls back to
|
||||||
|
* the snapshot.
|
||||||
|
*
|
||||||
|
* TRACES: UR-040, UR-005 | DR-052 | UT-060
|
||||||
|
*/
|
||||||
|
export function shouldResumeOnForeground(
|
||||||
|
wasPlaying: boolean,
|
||||||
|
nativeStateKind: string | undefined
|
||||||
|
): boolean {
|
||||||
|
return wasPlaying && nativeStateKind !== "paused";
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user