Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 0 additions & 82 deletions Apps/PointerLocker/BackgroundKeepAlive.swift

This file was deleted.

93 changes: 0 additions & 93 deletions Apps/PointerLocker/BackgroundSessionKeeper.swift

This file was deleted.

21 changes: 10 additions & 11 deletions Apps/PointerLocker/BrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import WebKit
/// 3. Hold Escape, ⌘., three-finger tap, app switch, navigation or the system
/// dropping the lock → page gets `pointerlockchange` and we release.
///
/// The menu lives in +Menu, popups/dialogs/permissions in +WebUI, and the
/// background keep-alive in BackgroundSessionKeeper.
/// The menu lives in +Menu, popups/dialogs/permissions in +WebUI.
final class BrowserViewController: UIViewController {
private(set) var webView: WKWebView!
/// Set by RootViewController; Settings uses it to switch services.
Expand All @@ -25,7 +24,6 @@ final class BrowserViewController: UIViewController {
private var crashGuard = CrashLoopGuard()
private var loadFailure: LoadFailure?
private let network = NWPathMonitor()
private var sessionKeeper: BackgroundSessionKeeper!
private var hudFeeder: DebugHUDFeeder!
#if DEBUG
private var debugBridge: DebugBridge?
Expand Down Expand Up @@ -77,7 +75,6 @@ final class BrowserViewController: UIViewController {
webView.evaluateJavaScript(script) { _, _ in completion() }
}

sessionKeeper = BackgroundSessionKeeper(webView: webView)
hudFeeder = DebugHUDFeeder(webView: webView, bridge: bridge) { [weak self] in
(self?.pageWantsLock ?? false, self?.isSystemPointerLocked ?? false)
}
Expand Down Expand Up @@ -171,8 +168,11 @@ final class BrowserViewController: UIViewController {
}

/// Which phase the page's session is in (none / queued-loading / playing).
func sessionPhase(_ completion: @escaping (BackgroundSessionKeeper.StreamPhase) -> Void) {
sessionKeeper.currentPhase(completion)
func sessionPhase(_ completion: @escaping (StreamPhase) -> Void) {
guard let webView else { return completion(.none) }
webView.evaluateJavaScript(ServiceProfile.current.sessionPhaseScript) { result, _ in
completion(StreamPhase(rawValue: result as? Int ?? 0) ?? .none)
}
}

/// Before being replaced (service switch): stop listening to the mouse,
Expand All @@ -181,7 +181,6 @@ final class BrowserViewController: UIViewController {
forceUnlock()
bridge.invalidate()
hudFeeder.stop()
sessionKeeper.willEnterForeground() // stops any keep-alive
health.resetSession()
#if DEBUG
debugBridge?.stop()
Expand Down Expand Up @@ -248,11 +247,11 @@ final class BrowserViewController: UIViewController {
observers.append(center.addObserver(forName: UIScene.willDeactivateNotification, object: nil, queue: .main) { [weak self] _ in
self?.forceUnlock()
})
// iPadOS suspends the app soon after it leaves the screen, which ends
// a game stream (docs/background-keep-alive.md has the keep-alive this
// replaced). Pause media now so nothing is cut off mid-sound.
observers.append(center.addObserver(forName: UIScene.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.sessionKeeper.didEnterBackground() }
})
observers.append(center.addObserver(forName: UIScene.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.sessionKeeper.willEnterForeground() }
MainActor.assumeIsolated { self?.webView?.pauseAllMediaPlayback() }
})
}

Expand Down
2 changes: 1 addition & 1 deletion Apps/PointerLocker/Services/FigmaProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension ServiceProfile {
// asks for the lock. Everything else it needs (WebGL 2, WebAssembly
// SIMD, workers, OffscreenCanvas) is in WebKit.
identity: .macChrome,
// No stream to keep alive; Figma saves as you go.
// No game session; Figma saves as you go.
sessionPhaseScript: "0",
tagline: "Collaborative design tool",
artwork: ServiceArtwork(
Expand Down
4 changes: 2 additions & 2 deletions Apps/PointerLocker/Services/ServiceCategory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ enum ServiceCategory: CaseIterable {
}
}

/// Games stream a session that must survive app switches (keep-alive,
/// voice chat); tools save as you go.
/// Games stream a session (switching services asks first, voice chat);
/// tools save as you go.
var hasGameSession: Bool { self == .play }

/// The services in this world, as a comma-separated list for Welcome.
Expand Down
14 changes: 11 additions & 3 deletions Apps/PointerLocker/Services/ServiceProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,18 @@ struct BrowserIdentity: Equatable {
)
}

/// Where a service's session is, read from the page by `sessionPhaseScript`.
/// A running session makes switching services ask first.
enum StreamPhase: Int {
case none = 0 // library, sign-in, …
case starting = 1 // in the queue or loading the game
case playing = 2 // a stream is playing
}

/// Everything specific to one streaming service ("engine"): where it lives,
/// which browser it needs to see, and how to tell that a session is running.
/// The rest of the app (pointer lock, fullscreen emulation, mouse input,
/// background keep-alive) is shared by all of them.
/// The rest of the app (pointer lock, fullscreen emulation, mouse input) is
/// shared by all of them.
///
/// Adding a service: define a profile in its own file (see
/// GeForceNowProfile.swift) and list it in `all`.
Expand All @@ -51,7 +59,7 @@ struct ServiceProfile {
let homeURL: URL
let identity: BrowserIdentity
/// JavaScript evaluating to the session phase:
/// 0 none, 1 queued or loading, 2 streaming (see BackgroundSessionKeeper).
/// 0 none, 1 queued or loading, 2 streaming (see StreamPhase).
let sessionPhaseScript: String
/// One line under the name on the service card.
let tagline: String
Expand Down
11 changes: 0 additions & 11 deletions Apps/PointerLocker/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,6 @@ enum Settings {
set { defaults.set(newValue.rawValue, forKey: "microphone") }
}

/// How long a stream keeps running after the app goes to the background
/// (e.g. to answer a message), in seconds; 0 pauses it right away.
static var backgroundKeepAlive: TimeInterval {
get { defaults.object(forKey: "backgroundKeepAlive") as? TimeInterval ?? 300 }
set { defaults.set(newValue, forKey: "backgroundKeepAlive") }
}

static let backgroundKeepAlivePresets: [(title: String, seconds: TimeInterval)] = [
("Off", 0), ("1 minute", 60), ("5 minutes", 300), ("15 minutes", 900),
]

/// Show the debug overlay (lock state, input, stream stats).
static var debugHUD: Bool {
get { defaults.bool(forKey: "debugHUD") }
Expand Down
7 changes: 0 additions & 7 deletions Apps/PointerLocker/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ struct SettingsView: View {

@State private var sensitivity = Settings.sensitivity
@State private var invertY = Settings.invertY
@State private var keepAlive = Settings.backgroundKeepAlive
@State private var microphone = Settings.microphone
@State private var useIdentity = Settings.useServiceIdentity
@State private var debugHUD = Settings.debugHUD
Expand Down Expand Up @@ -45,16 +44,11 @@ struct SettingsView: View {
}
if service.category.hasGameSession {
Section {
Picker("Keep game running in background", selection: $keepAlive) {
ForEach(Settings.backgroundKeepAlivePresets, id: \.seconds) { Text($0.title).tag($0.seconds) }
}
Picker("Microphone", selection: $microphone) {
ForEach(MicrophoneAccess.allCases, id: \.self) { Text($0.title).tag($0) }
}
} header: {
Text("Game session")
} footer: {
Text("Keeping the game running lets you switch apps briefly without losing your session.")
}
}
Section {
Expand Down Expand Up @@ -110,7 +104,6 @@ struct SettingsView: View {
}
.onChange(of: sensitivity) { Settings.sensitivity = sensitivity }
.onChange(of: invertY) { Settings.invertY = invertY }
.onChange(of: keepAlive) { Settings.backgroundKeepAlive = keepAlive }
.onChange(of: microphone) { Settings.microphone = microphone }
.onChange(of: useIdentity) { Settings.useServiceIdentity = useIdentity }
.onChange(of: debugHUD) { Settings.debugHUD = debugHUD }
Expand Down
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,16 @@ NOW, Figma): pick one to switch straight to it. Each service reopens the page yo
were last on, such as the open Figma file; a running game asks before you leave.
Below that are Back, Reload, Home, Open URL and **Settings…**: switch service
(with the setup guide), show the setup guide again, mouse sensitivity, invert
Y-axis, background keep-alive, microphone, browser identity, the debug overlay,
Y-axis, microphone, browser identity, the debug overlay,
service checks, and About (version, links to the website and this
repository). The button hides while the pointer is locked.

**Switching apps mid-game.** iPadOS suspends an app within seconds of leaving
it, which drops the stream and ends the session. Mouselook keeps itself
running in the background during a session. While you wait in the queue or the
game loads, it keeps running until the game starts, for up to 30 minutes. While
a stream plays, it keeps running for 5 minutes by default (⋯ ▸ Settings… ▸ Keep game
running in background: Off / 1 / 5 / 15 minutes). It does this by playing silence,
since WebKit pauses a quiet stream in the background. When the time is up it
pauses the stream so the system can suspend the app. GeForce NOW still needs a
tap on Resume when you come back.
it, which drops the stream, so leaving Mouselook mid-game can end the session.
Mouselook pauses media when you leave. Earlier builds kept the app running in
the background by playing silence; App Review doesn't allow that (guideline
2.5.4), so it was removed. [docs/background-keep-alive.md](docs/background-keep-alive.md)
has how it worked, in case it comes back in an allowed form.

**Volume won't go to zero?** If you allowed the microphone, WebKit uses the
voice-call audio mode, in which iPadOS keeps a minimum volume. Set ⋯ ▸ Settings… ▸
Expand Down Expand Up @@ -227,7 +224,6 @@ builds and tests the app.
| `Resources/pointerlock-polyfill.js` | Pointer Lock and Fullscreen APIs in the page. It applies the profile's identity, including inside Web Workers, and turns native input into DOM events. |
| `MouseBridge.swift` | Raw GCMouse input to the page, one batch per frame, never more than one in flight. |
| `BrowserViewController*.swift` | The web view and lock state (+Menu, +WebUI for popups, dialogs and permissions). |
| `BackgroundSessionKeeper.swift`, `BackgroundKeepAlive.swift` | Keeping a session alive across app switches. |
| `PageRecovery.swift`, `StatusOverlayView.swift` | Crashes, failed loads and going offline. |
| `DebugHUDFeeder.swift`, `Resources/debug-hud.js`, `DebugBridge.swift` | The debug overlay, and the Mac-side JavaScript console (Debug builds only). |

Expand Down
Loading
Loading