feat(playback): add external playback command stream - #194
feat(playback): add external playback command stream#194AndreasLymalmMTM wants to merge 9 commits into
Conversation
After goForward(), locators.last may differ from savedLocator while still having the same href. Therefore, we must ensure a new locator is emitted after calling goToLocator().
|
Hey @AndreasLymalmMTM , sorry for letting this one marinate a bit, we had some overlapping holidays. Could you update it to latest main, then we'll get it merged. |
There was a problem hiding this comment.
Pull request overview
Adds a new cross-platform event stream to the Flutter Readium plugin that surfaces user/system media-control intent (headphones, lock screen, Control Center, Android media session) as structured commands, distinct from the existing timebased playback state stream.
Changes:
- Introduces
ReadiumExternalPlaybackCommand+ExternalPlaybackCommandActionin the platform interface and exposesonExternalPlaybackCommandonFlutterReadium. - Implements native emission on iOS (MPRemoteCommandCenter) and Android (Media3 forwarding player + custom notification commands), with Web exposing the stream for compatibility.
- Updates example UI + docs + changelogs, and adds unit tests for parsing/stream wiring.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| flutter_readium/test/flutter_readium_test.dart | Adds mock stream + unit test for the new Dart-facing API. |
| flutter_readium/lib/src/flutter_readium_web.dart | Exposes the stream on Web for API compatibility (no emission yet). |
| flutter_readium/lib/flutter_readium.dart | Adds public onExternalPlaybackCommand getter + docs. |
| flutter_readium/ios/.../navigator/NowPlayingInfoUpdater.swift | Emits commands from iOS system media controls. |
| flutter_readium/ios/.../model/ReadiumExternalPlaybackCommand.swift | Adds iOS model + mapping to event payload. |
| flutter_readium/ios/.../FlutterReadiumPlugin.swift | Registers/disposes event channel + provides emitter method. |
| flutter_readium/example/lib/widgets/timebased.state.widget.dart | Wires example UI to display last external command. |
| flutter_readium/example/lib/widgets/external_playback_command_status.widget.dart | New widget showing commands for a short duration. |
| flutter_readium/example/lib/state/player_controls_bloc.dart | Exposes the new stream via bloc getter. |
| flutter_readium/CHANGELOG.md | Documents new stream in plugin changelog. |
| flutter_readium/android/.../ReadiumReader.kt | Adds Android event channel plumbing + emitter entrypoint. |
| flutter_readium/android/.../PluginMediaService.kt | Emits commands based on Media3 control operations. |
| flutter_readium/android/.../events/ReadiumExternalPlaybackCommandEventChannel.kt | New Android event channel + command payload model. |
| flutter_readium_platform_interface/test/models_test.dart | Tests enum parsing + JSON round-trip + unknown fallback. |
| flutter_readium_platform_interface/test/flutter_readium_platform_interface_test.dart | Tests MethodChannel stream wiring & subscription behavior. |
| flutter_readium_platform_interface/lib/src/index.dart | Exports the new model. |
| flutter_readium_platform_interface/lib/src/external_playback_command.dart | Adds shared command model + enum parsing/serialization. |
| flutter_readium_platform_interface/lib/method_channel_flutter_readium.dart | Adds event channel + decoding for the new stream. |
| flutter_readium_platform_interface/lib/flutter_readium_platform_interface.dart | Adds abstract onExternalPlaybackCommand contract. |
| flutter_readium_platform_interface/CHANGELOG.md | Documents new shared API in interface changelog. |
| docs/guides/audiobook-playback.md | Adds usage guidance for the new stream. |
| docs/getting-started/concepts.md | Updates streams overview to include the new stream. |
| docs/api-reference/streams-events.md | Adds API reference section for onExternalPlaybackCommand. |
| docs/api-reference/flutter-readium.md | Adds stream to the API reference table. |
Suppressed comments (1)
flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/NowPlayingInfoUpdater.swift:238
nextTrackCommandcurrently emits.nextbut then callsnavigator.seekForward(). This makes the emitted action inconsistent with the behavior being performed (track skip vs seek-forward). If this handler is meant to perform a seek, emit.seekForwardinstead.
emit(.next)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
They should represent the actual external command, not what the software will do.
Description
This adds a new stream
onExternalPlaybackCommand, that emits playback commands received from headphones, lock screen, etc., such as play, pause, and seeking. It works on iOS and Android.What Problem does this Solve?
The existing stream
onTimebasedPlayerStateChangedgives the current playback state (pause, play, etc.). However, these are emitted when something internal happens as well, e.g. pause is emitted when chapter/file is changed automatically.Sometimes we would rather want to know when the user actively has performed an action (e.g. play, pause on headphone) and act upon that, and ignore any internal state changes.
See related issue: #40
Example
There is now text in the example project that shows what is received from the stream, displayed for 2 seconds. In the video, Apple Airpods Pro were used to pause, play, seek forward and backward. (Notice how "State" very quickly emits
pauseandloadingwhen seeking, which comes from internal state changes.)flutter_readium-external-playback-command.mp4