An iOS app: the page in a web view, with the phone's own player (#418)
web/src/native.ts (#45) has handed playback to a host since September, and there was no host in this repo: the earlier app, rays/ipodderx-app, grew native lists beside the page and is left behind. This one is the page and a player: a WKWebView of ipx-web, AVPlayer behind the bridge with the audio session to play on locked, now playing and the remote commands (15 back, 30 forward, scrubbing), and positions and read written from the player's own clock, with DeadAir's copy named so the server reads the position on its clock. Built with XcodeGen on a Mac; nothing here compiles the Swift. Its icon is web/logo.svg at 1024. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqkrVDqdiQEMWAkNGQ2tYA
This commit is contained in:
79
ios/README.md
Normal file
79
ios/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# iPX for iOS
|
||||
|
||||
ipx's page in an app, with the audio played by the phone rather than the page: it plays on with
|
||||
the screen locked, and the lock screen, Control Center, headphones and a car over Bluetooth or USB
|
||||
drive it. Everything you see is the page, as on the home screen; the app adds the player behind it.
|
||||
|
||||
## Build it
|
||||
|
||||
On a Mac with Xcode:
|
||||
|
||||
```sh
|
||||
brew install xcodegen
|
||||
cd ios && xcodegen generate && open iPX.xcodeproj
|
||||
```
|
||||
|
||||
Set your team under Signing & Capabilities, pick your phone, and run. The project is generated
|
||||
from `project.yml`, so `.xcodeproj` is not in git: edit the yml. Nothing on Tower can build it.
|
||||
|
||||
A free personal team works, but a build on it expires after seven days and is reinstalled from
|
||||
Xcode. A paid account ($99 a year) makes that a year and adds TestFlight. Playing with the screen
|
||||
locked is a plist key (`UIBackgroundModes: audio`), not an entitlement, so it works on either.
|
||||
CarPlay's own dashboard app does not: Apple grants `com.apple.developer.carplay-audio` on request.
|
||||
|
||||
## Which server
|
||||
|
||||
The first launch asks, with `https://ipodderx.sdf1.net` filled in; a bare host gets `https://`.
|
||||
Shake the phone to change it. Plain `http://` only on your own network
|
||||
(`http://192.168.1.130:8099`), which is all `NSAllowsLocalNetworking` allows. Sign in on the page
|
||||
that follows, through Cloudflare Access or ipx's own form; the cookies that leaves are what the
|
||||
player uses (`CookieBridge`). `-ipx.server <url>` as a launch argument overrides it for a run.
|
||||
|
||||
## How it hangs together
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `Sources/App.swift` | the app and its one scene |
|
||||
| `Sources/WebViewController.swift` | the `WKWebView`, the server question, a page that will not load |
|
||||
| `Sources/Bridge.swift` | the messages to and from `web/src/native.ts` |
|
||||
| `Sources/Playback.swift` | `AVPlayer`, the audio session, now playing, the remote commands, saving positions |
|
||||
| `Sources/CookieBridge.swift` | the web view's cookies into `HTTPCookieStorage.shared`, for the player |
|
||||
| `Sources/Server.swift` | which server |
|
||||
|
||||
## The messages
|
||||
|
||||
The page's half is `web/src/native.ts`, and `node tests/native-bridge.ts` holds it to these. A
|
||||
change to them is a change to both halves in one commit, and the page has to be deployed before
|
||||
an app that needs it.
|
||||
|
||||
Page to app, on `webkit.messageHandlers.ipx`:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `{t:"ready", version, rate, volume}` | the bridge is in |
|
||||
| `{t:"load", url, enc, feedId, guid, title, feedTitle, artwork, position, duration, rate, volume}` | the element's `src` was set to an audio file |
|
||||
| `{t:"play"}` `{t:"pause"}` `{t:"stop"}` | |
|
||||
| `{t:"seek", to}` `{t:"rate", v}` `{t:"volume", v}` | |
|
||||
| `{t:"position", url}` | save where we are; a path, not a time, since a page frozen in the background holds an old one |
|
||||
|
||||
App to page, as `window.ipxNative.on(...)`:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `{t:"time", cur, dur}` | `timeupdate` |
|
||||
| `{t:"meta", dur}` | `loadedmetadata`: the page seeks to where you left off |
|
||||
| `{t:"state", playing}` | `play` or `pause` |
|
||||
| `{t:"ended"}` | |
|
||||
| `{t:"error", message}` | the page says so |
|
||||
|
||||
The app writes the position (every ten seconds and on pause) and marks an item read at 90%, from
|
||||
its own clock, so both happen while the page is frozen. Video stays in the page.
|
||||
|
||||
## Not yet
|
||||
|
||||
* Episodes kept on the device (#412) are the page's service worker's, which a `WKWebView` does
|
||||
not run without App-Bound Domains; the app plays them from the server.
|
||||
* The cookie is the only credential: when Cloudflare Access's expires mid-drive, playback stops
|
||||
with "Sign in again", and opening the app signs in. A token in the keychain would end that.
|
||||
* CarPlay's dashboard app needs Apple's entitlement first; then it is a second scene over the same
|
||||
`Playback`.
|
||||
Reference in New Issue
Block a user