Skip to content

Add background audio and lock screen player (iOS) - #2

Draft
webguy-nz wants to merge 1 commit into
NativePHP:mainfrom
webguy-nz:feat/now-playing-remote-controls
Draft

Add background audio and lock screen player (iOS)#2
webguy-nz wants to merge 1 commit into
NativePHP:mainfrom
webguy-nz:feat/now-playing-remote-controls

Conversation

@webguy-nz

Copy link
Copy Markdown

Populates MPNowPlayingInfoCenter and registers MPRemoteCommandCenter handlers, so playback appears on the lock screen, in Control Center and in the Dynamic Island with working transport controls.

iOS only. There is no Android counterpart in this PR — the Kotlin side is untouched.

API

play() accepts optional title, artist and artwork:

MediaPlayer::play('https://example.com/live.m3u8', [
    'title' => 'The Platform',
    'artist' => 'Live stream',
    'artwork' => 'https://example.com/logo.png', // remote URL or local path
]);

Remote artwork is fetched off the main thread and applied when it arrives, so playback never waits on it. Omitted keys are not sent, so existing play() calls are unaffected.

A new RemoteCommand event reports lock screen presses back to PHP so in-app UI can reconcile. The native side has already applied the command by the time the event fires — handlers exist to sync UI, not to perform the action.

Live streams

Verified on a physical iPhone 15 Pro against a live HLS radio stream. Live sources needed specific handling:

  • A source with a non-finite duration is published with IsLiveStream, which makes iOS render a LIVE badge with no scrubber. It also makes the system show a stop button rather than pause/play, so pauseCommand is never delivered for live audio and a halt arrives as stopCommand.
  • Remote stop therefore releases the player, since a live buffer is stale the moment it stops, but keeps the Now Playing card so the user can start again from the lock screen. Deactivating the audio session here would hand the Now Playing slot to whatever played previously and the card would vanish.
  • Remote play restarts from the last source when the player has been released, and seeks to the live edge when it is merely paused, rather than resuming a stale buffer.

Background audio is left to the consuming app

The audio background mode is deliberately not declared in the manifest: Apple rejects apps that claim a background mode they don't use, and a video-only consumer must not inherit it. Consumers opt in from their own config/nativephp.php:

'permissions' => [
    'UIBackgroundModes' => ['audio'],
],

The README documents this, and a test pins the omission so it can't be added back by accident.

This is the main design decision I'd like a second opinion on before this comes out of draft — happy to flip it to declared-by-default if you'd rather the plugin own it.

Worth knowing when testing: the iOS simulator keeps playing audio in the background whether or not the mode is declared. Needs a physical device to verify.

Testing

vendor/bin/pest — 42 passed, 126 assertions. New coverage in tests/NowPlayingTest.php plus a manifest test for the background-mode omission. The 2 warnings on the existing off-device bridge tests are pre-existing (no bridge listening on 127.0.0.1:3002 outside the runtime).

Manually verified on an iPhone 15 Pro: lock screen, Control Center and Dynamic Island transport controls against both a live HLS stream and a finite-duration file.

Populate MPNowPlayingInfoCenter and register MPRemoteCommandCenter handlers
so playback appears on the lock screen, in Control Center and in the Dynamic
Island, with working transport controls.

play() accepts optional title / artist / artwork. Remote artwork is fetched
off the main thread and applied when it arrives, so playback never waits on
it. A new RemoteCommand event reports lock screen presses back to PHP so
in-app UI can reconcile; the native side has already applied the command.

Live streams needed specific handling, verified on an iPhone 15 Pro against a
live HLS radio stream:

- A source with a non-finite duration is published with IsLiveStream, which
  makes iOS render a LIVE badge with no scrubber. It also makes the system
  show a stop button rather than pause/play, so pauseCommand is never
  delivered for live audio and a halt arrives as stopCommand.
- Remote stop therefore releases the player, since a live buffer is stale the
  moment it stops, but keeps the Now Playing card so the user can start again
  from the lock screen. Deactivating the audio session here would hand the
  Now Playing slot to whatever played previously and the card would vanish.
- Remote play restarts from the last source when the player has been
  released, and seeks to the live edge when it is merely paused, rather than
  resuming a stale buffer.

The audio background mode is deliberately left to the consuming app rather
than declared in the manifest: Apple rejects apps that claim a background
mode they do not use, and a video-only consumer must not inherit it. The
README documents the one-line config opt-in, and a test pins the omission.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant