The page cannot hand playback to a native app #45

Closed
opened 2026-09-19 14:23:27 -07:00 by rays · 1 comment
Owner

For an iOS app that is a web view of the UI but plays in the car.

CarPlay and Android Auto cannot render a web view. Both are template surfaces, 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 the page has no way to give playback away.

web/src/player.ts drives its media element directly, so inside a native shell there would be two engines on one file, fighting over the same position writes. player.ts is already emphatic about what a stale write costs -- "one left paused in another tab saved its older place as that tab reloaded, over where you had got to since" -- and a backgrounded web view is exactly that tab.

What is missing is a bridge: a web/src/native.ts that, when the page is running inside a native shell, replaces the element's playback surface (play, pause, src, currentTime, duration, paused, readyState, volume, playbackRate, and the events) with one that forwards to the host and synthesises the events back. player.ts is then not touched at all, because it only ever speaks to the element, and the player bar, the row buttons, the EQ bars and the keyboard shortcuts keep working. Video stays in the page: CarPlay is audio-only, and a native video layer under a web view buys nothing. In a browser nothing installs.

Nothing in src/ changes. /media/{id} already serves Range through ServeFile, and the position and flags endpoints are what the host writes back through -- the auth layer's own note in web.rs says it was built so a plain <audio src> would work off cookies alone, which is exactly the property the native player needs.

The app lives in its own repo, ipodderx-ios.

For an iOS app that is a web view of the UI but plays in the car. CarPlay and Android Auto cannot render a web view. Both are template surfaces, 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 the page has no way to give playback away. `web/src/player.ts` drives its media element directly, so inside a native shell there would be two engines on one file, fighting over the same position writes. player.ts is already emphatic about what a stale write costs -- "one left paused in another tab saved its older place as that tab reloaded, over where you had got to since" -- and a backgrounded web view is exactly that tab. What is missing is a bridge: a `web/src/native.ts` that, when the page is running inside a native shell, replaces the element's playback surface (`play`, `pause`, `src`, `currentTime`, `duration`, `paused`, `readyState`, `volume`, `playbackRate`, and the events) with one that forwards to the host and synthesises the events back. player.ts is then not touched at all, because it only ever speaks to the element, and the player bar, the row buttons, the EQ bars and the keyboard shortcuts keep working. Video stays in the page: CarPlay is audio-only, and a native video layer under a web view buys nothing. In a browser nothing installs. Nothing in `src/` changes. `/media/{id}` already serves Range through `ServeFile`, and the position and flags endpoints are what the host writes back through -- the auth layer's own note in `web.rs` says it was built so a plain `<audio src>` would work off cookies alone, which is exactly the property the native player needs. The app lives in its own repo, `ipodderx-ios`.
rays added the enhancement label 2026-09-19 14:23:27 -07:00
Author
Owner

Fixed in 9d1492b. web/src/native.ts replaces the playback surface of the page's media element with one that forwards to a native host and synthesises the events back, so player.ts is unchanged and the player bar, row buttons and keyboard shortcuts keep working. Video stays in the page. In a browser nothing installs.

Position and read are the host's to write, because a backgrounded web view is frozen and holds a time from minutes ago.

tests/native-bridge.js holds the two ends together; tests/dom-stub.js is the stub DOM it shares with page-smoke. It caught a real one straight away: the src setter called removeAttribute('src'), which the shim's own override turned into a stop() that switched it back off one line after enabling it.

Verified end to end on an iOS shell against a fixture daemon: tapping play produced GET /media/5 -> 206 from AVPlayer with the session cookie, and the episode came back read.

Fixed in 9d1492b. `web/src/native.ts` replaces the playback surface of the page's media element with one that forwards to a native host and synthesises the events back, so `player.ts` is unchanged and the player bar, row buttons and keyboard shortcuts keep working. Video stays in the page. In a browser nothing installs. Position and read are the host's to write, because a backgrounded web view is frozen and holds a time from minutes ago. `tests/native-bridge.js` holds the two ends together; `tests/dom-stub.js` is the stub DOM it shares with page-smoke. It caught a real one straight away: the src setter called `removeAttribute('src')`, which the shim's own override turned into a `stop()` that switched it back off one line after enabling it. Verified end to end on an iOS shell against a fixture daemon: tapping play produced `GET /media/5 -> 206` from AVPlayer with the session cookie, and the episode came back read.
rays closed this issue 2026-09-19 15:51:11 -07:00
rays referenced this issue from a commit 2026-09-19 16:29:12 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipodderx-rs#45