# ipodderx-app
The [ipodderx-rs](https://git.sdf1.net/rays/ipodderx-rs) web UI in an app, with the audio played by
the phone rather than the page, so it keeps going when the screen locks and a car can control it.
## Why it is not just a web view
CarPlay and Android Auto cannot render a web view. Both are template surfaces — `CPListTemplate`
and `CPNowPlayingTemplate`, or Android's media browse tree — and the only audio they will control
is the host's own `AVPlayer` or `ExoPlayer`. So an app that is "the web UI plus CarPlay" is really
"the web UI whose audio engine is native", and that is what this is.
The page keeps its face. Everything in ipx's `web/src/player.ts` speaks to its media element
through a small surface, so `web/src/native.ts` replaces that surface on the element with one that
posts to this app. The player bar, the row buttons, the EQ bars and the keyboard shortcuts all work
as they do in a browser, with nothing in `player.ts` changed. Video still plays in the page:
CarPlay is audio-only, and a native video layer under a web view buys nothing.
## What works, and what needs an Apple account
| | |
|---|---|
| Plays with the screen locked | Yes, on a free personal team. `UIBackgroundModes: [audio]` is a plist key, not a signed entitlement |
| Lock screen and Control Center | Yes — `MPNowPlayingInfoCenter`, `MPRemoteCommandCenter` |
| A car over Bluetooth or USB audio | Yes — the head unit shows the episode and its buttons drive the app |
| CarPlay's own app on the dashboard | **No.** `com.apple.developer.carplay-audio` is granted by Apple on request and does not exist on a free team |
A free personal team also expires a build after seven days, so it is reinstalled from Xcode
weekly, and allows three apps at a time. $99/yr makes that a year and adds TestFlight; CarPlay
additionally needs a granted entitlement request at .
`Playback` and `Library` deliberately know nothing about the web view. A CarPlay scene is a
`CPTemplateApplicationSceneDelegate` over the same two objects, plus a browse tree built from
`GET /api/feeds` and `GET /api/entries` — no change to this app's shape or to the server.
## Build it
```sh
brew install xcodegen
cd ios && xcodegen generate
open iPodderX.xcodeproj
```
Pick a destination and run. **My Mac (Mac Catalyst)** is one of them: the same UIKit app in a
window, built from the same target. Nothing in the Swift is conditional -- `AVAudioSession`,
`MPNowPlayingInfoCenter` and the remote commands all exist under Catalyst -- so the Mac gets
media keys and Now Playing in Control Center for free. It has no CarPlay and no lock screen,
which is most of the point on a phone, so the Mac build is a convenience rather than the reason
any of this exists.
`SUPPORTS_MACCATALYST` is set as a build setting rather than through xcodegen's
`supportsMacCatalyst:`, which this version accepts and then writes nothing for -- the generated
project had no such setting and the Mac destination simply did not exist.
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, not the project.
## Which server
The first launch asks, with `https://ipodderx.sdf1.net` filled in. A bare host gets `https://`, so
typing `ipodderx.sdf1.net` is enough. Then sign in on the page that follows — through Cloudflare
Access, or ipx's own form — and the cookies that leaves are what the player uses.
**Shake the phone to change it**, or tap **Server** in the red banner when something is wrong.
There is no button in the chrome because there is no chrome: the page fills the screen, and this
is a setting touched about once.
Plain `http://` is allowed only on your own network — `localhost`, `*.local`, and the private
ranges — which is what `NSAllowsLocalNetworking` covers. The setup screen says so as you type
rather than letting the load fail later looking like the server is down.
For automation, `-ipx.server ` as a launch argument overrides the stored value for that run.
## How it hangs together
| | |
|---|---|
| `ios/Sources/App.swift` | the app delegate, and which scene is which |
| `ios/Sources/SceneDelegate.swift` | the window. iOS 27 will not run an app without a scene, and CarPlay is a second one |
| `ios/Sources/ServerSetupViewController.swift` | the first-run question, and the way back to it |
| `ios/Sources/WebViewController.swift` | the `WKWebView`, and the banner when something is wrong |
| `ios/Sources/Bridge.swift` | the messages, both directions |
| `ios/Sources/Playback.swift` | `AVPlayer`, the audio session, now-playing, the remote commands |
| `ios/Sources/Library.swift` | the API calls the host makes: position, read, artwork |
| `ios/Sources/CookieBridge.swift` | `WKHTTPCookieStore` into `HTTPCookieStorage.shared` |
| `ios/Sources/ServerSettings.swift` | which server |
### Authentication
ipx authenticates by cookie: `ipx_session` from its own sign-in, and `CF_Authorization` from
Cloudflare Access in front of the tunnel. Its auth layer was written that way so a plain
`