1+ import Foundation
2+
13// MARK: - SingletonPlayerWebView Media Controls
24
35extension SingletonPlayerWebView {
@@ -15,6 +17,45 @@ extension SingletonPlayerWebView {
1517 Self . mediaControlStyleBootstrapScript ( useNextPrev: self . mediaControlUsesNextPrev)
1618 }
1719
20+ /// Re-asserts Kaset's `nexttrack`/`previoustrack` media-session override immediately.
21+ ///
22+ /// YouTube Music periodically re-registers its own handlers. In `nextPreviousTrack`
23+ /// mode the page keeps ownership via a `requestAnimationFrame` re-apply loop — but
24+ /// WebKit freezes `requestAnimationFrame` while the app is backgrounded, so the
25+ /// override is lost and a media-key press falls through to YouTube (which jumps to its
26+ /// own recommendation; queue-drift recovery then restarts the current song from 0).
27+ /// Driving the re-apply from a native timer keeps the override alive in the background.
28+ func reassertMediaControlOverride( ) {
29+ guard self . mediaControlUsesNextPrev, let webView = self . webView else { return }
30+ webView. evaluateJavaScript (
31+ " if (typeof window.__kasetRefreshMediaControlStyle === 'function') { window.__kasetRefreshMediaControlStyle(); } " ,
32+ completionHandler: nil
33+ )
34+ }
35+
36+ /// Starts a native timer that re-asserts the media-key override while the app is
37+ /// backgrounded. Native run-loop timers keep firing in the background (active audio
38+ /// playback prevents App Nap), unlike the page's frozen `requestAnimationFrame` loop.
39+ func beginBackgroundMediaControlReassertion( ) {
40+ guard self . mediaControlUsesNextPrev else { return }
41+ self . reassertMediaControlOverride ( )
42+ guard self . mediaControlReassertTimer == nil else { return }
43+ let timer = Timer . scheduledTimer ( withTimeInterval: 2.0 , repeats: true ) { [ weak self] ( _: Timer ) in
44+ MainActor . assumeIsolated {
45+ self ? . reassertMediaControlOverride ( )
46+ }
47+ }
48+ timer. tolerance = 0.5
49+ self . mediaControlReassertTimer = timer
50+ }
51+
52+ /// Stops the background re-assertion timer. The page's `requestAnimationFrame` loop
53+ /// resumes ownership once the app is foreground again.
54+ func endBackgroundMediaControlReassertion( ) {
55+ self . mediaControlReassertTimer? . invalidate ( )
56+ self . mediaControlReassertTimer = nil
57+ }
58+
1859 static func mediaControlStyleBootstrapScript( useNextPrev: Bool ) -> String {
1960 let jsBoolean = useNextPrev ? " true " : " false "
2061 return """
0 commit comments