iPodderX for iPhone, iPad and Mac
A shell around the ipodderx-rs web UI whose audio is played by the host rather than the page, so it carries on with the screen locked and a car can control it. The page's half of the bridge is web/src/native.ts in ipodderx-rs (#45): it replaces the playback surface of the page's media element with one that posts here, so player.ts is unchanged and the player bar, the row buttons and the keyboard shortcuts work as they always did. Playback, Library, Bridge and CookieBridge never touch a view, which is what makes a CarPlay scene a later addition rather than a rewrite, and what lets the same target build for Mac Catalyst with no conditional code: AVAudioSession, MPNowPlayingInfoCenter and the remote commands all exist there. The Mac gets media keys and Now Playing; it has no CarPlay and no lock screen, so it is a convenience rather than the reason for any of this. Authentication is the web view's. ipx decides who is asking by cookie -- ipx_session, and CF_Authorization from Cloudflare Access in front of the tunnel -- and its auth layer was written so a plain <audio src> would work, which is why the player needs no API of its own. CookieBridge keeps HTTPCookieStorage in step with the web view's store and hands them to each asset. Named ipodderx-app, not -ios: native.ts already carries the branch for an Android host, and the iOS project is one directory rather than the repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
46
ios/Sources/CookieBridge.swift
Normal file
46
ios/Sources/CookieBridge.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
/// The web view signs in; everything else rides on what that left behind.
|
||||
///
|
||||
/// ipx authenticates a request by cookie -- `ipx_session` from its own sign-in form, and
|
||||
/// `CF_Authorization` from Cloudflare Access in front of the tunnel. The auth layer was built that
|
||||
/// way so a plain `<audio src>` would work, and it is why the native player needs no API of its
|
||||
/// own. But `AVPlayer` and `URLSession` read `HTTPCookieStorage.shared`, and `WKWebView` keeps its
|
||||
/// own store, so the two have to be kept in step.
|
||||
final class CookieBridge: NSObject, WKHTTPCookieStoreObserver {
|
||||
private let store: WKHTTPCookieStore
|
||||
private(set) var cookies: [HTTPCookie] = []
|
||||
|
||||
init(store: WKHTTPCookieStore) {
|
||||
self.store = store
|
||||
super.init()
|
||||
store.add(self)
|
||||
sync()
|
||||
}
|
||||
|
||||
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) { sync() }
|
||||
|
||||
/// Copy everything across. Cheap, and it runs only when the web view's store changes --
|
||||
/// a sign-in, or Access handing out a fresh token.
|
||||
func sync(then done: (() -> Void)? = nil) {
|
||||
store.getAllCookies { [weak self] all in
|
||||
guard let self else { return }
|
||||
self.cookies = all
|
||||
for c in all { HTTPCookieStorage.shared.setCookie(c) }
|
||||
done?()
|
||||
}
|
||||
}
|
||||
|
||||
/// The cookies for one request, as `AVURLAsset` wants them.
|
||||
func cookies(for url: URL) -> [HTTPCookie] {
|
||||
cookies.filter { c in
|
||||
guard let host = url.host else { return false }
|
||||
let domain = c.domain.hasPrefix(".") ? String(c.domain.dropFirst()) : c.domain
|
||||
guard host == domain || host.hasSuffix("." + domain) else { return false }
|
||||
// A Secure cookie over plain HTTP is not sent, which is the whole point of the flag.
|
||||
if c.isSecure && url.scheme != "https" { return false }
|
||||
return url.path.hasPrefix(c.path) || c.path == "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user