From 3c79406bbf51f1407b72bdb7670ce09a92fb7ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wyczarski?= Date: Mon, 16 Feb 2026 13:55:43 +0100 Subject: [PATCH 1/3] Simplify the API by removing AudioSource models --- .github/workflows/publish.yml | 1 + CHANGELOG.md | 27 +++++++ CLAUDE.md | 4 +- README.md | 68 ++++++++--------- example/lib/pages/home_page.dart | 15 ++-- example/lib/pages/queue_page.dart | 4 +- .../example_android_auto_delegate.dart | 2 +- .../providers/example_carplay_delegate.dart | 10 +-- example/pubspec.lock | 6 +- lib/mt_audio.dart | 1 - .../mt_android_auto_delegate.dart | 2 +- lib/src/carplay/mt_carplay_delegate.dart | 2 +- lib/src/handler/mt_audio_handler.dart | 73 +++++++++++++------ lib/src/models/mt_audio_source.dart | 71 ------------------ lib/src/player/mt_audio_player.dart | 20 ++--- pubspec.lock | 4 +- 16 files changed, 140 insertions(+), 170 deletions(-) delete mode 100644 lib/src/models/mt_audio_source.dart diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0a7fa53..d928631 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,7 @@ on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-**" permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 8964210..3dbb67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## 0.2.0-beta.3 - 2026-02-16 + +### Changed + +- Replaced `MtAudioSource` sealed class (`MtSingleSource`, `MtPlaylistSource`, `MtLiveSource`) with two explicit methods on `MtAudioPlayer`: `setAudioItem(MtAudioItem)` and `setPlaylist(List, {int initialIndex})`. + +### Breaking changes + +- **Removed** `MtAudioSource`, `MtSingleSource`, `MtPlaylistSource`, and `MtLiveSource` classes. +- **Removed** `MtAudioPlayer.setSource(MtAudioSource)`. +- **Added** `MtAudioPlayer.setAudioItem(MtAudioItem)` -- replaces `setSource(MtSingleSource(...))` and `setSource(MtLiveSource(...))`. +- **Added** `MtAudioPlayer.setPlaylist(List, {int initialIndex})` -- replaces `setSource(MtPlaylistSource(...))`. + +#### Migration + +```dart +// Before +await player.setSource(MtSingleSource(item: track)); +await player.setSource(MtLiveSource(item: stream)); +await player.setSource(MtPlaylistSource(items: tracks, initialIndex: 2)); + +// After +await player.setAudioItem(track); +await player.setAudioItem(stream); +await player.setPlaylist(tracks, initialIndex: 2); +``` + ## 0.2.0-beta.2 - 2026-02-13 Reclassified `mt_audio` as a beta release while testing is ongoing. diff --git a/CLAUDE.md b/CLAUDE.md index 0806d00..4443a92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ There are no tests, code generation, or build_runner steps in this module. 5. **CarPlay uses `mt_carplay`**: `MtCarPlayHandler` manages the connection lifecycle, navigation stack (max 5 deep), template rendering (list + grid + tab bar), and automatic playback state sync via player streams. -6. **Sealed class for audio sources**: `MtAudioSource` is a sealed class with three variants: `MtSingleSource`, `MtPlaylistSource`, `MtLiveSource`. +6. **Two-method source API**: Audio sources are set via `setAudioItem(MtAudioItem)` for single items (both regular tracks and live streams) and `setPlaylist(List, {initialIndex})` for playlists. The live/non-live distinction is encoded in `MtAudioItem.isLive`. 7. **`MtAudioItem` ↔ `MediaItem` conversion**: `MtAudioItem` stores URI, headers, and `isLive` flag in the `extras` map of `MediaItem` for round-trip conversion. The `uri` field is stored as `extras['uri']`, not `MediaItem.id`. @@ -92,7 +92,7 @@ Queue state tracks `shuffleIndices` separately. `MtAudioHandler.getQueueIndex()` - Uses `very_good_analysis` linter rules with relaxed settings: no 80-char line limit, no public API docs requirement, trailing commas preserved - All model classes extend `Equatable` -- Sealed classes for type-safe variants (`MtAudioSource`, `MtCarPlayItem`) +- Sealed classes for type-safe variants (`MtCarPlayItem`) - All classes prefixed with `Mt` (Mobitouch namespace) - Widgets take `MtAudioPlayer player` as required parameter - The example app uses a simple `InheritedWidget` (`PlayerProvider`) — not Riverpod diff --git a/README.md b/README.md index 4c0f5f9..94f5d31 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ ![Stability](https://img.shields.io/badge/stability-beta-orange) ![License](https://img.shields.io/badge/license-MIT-green) -A beta, streams-based audio module for Flutter. Provides background playback, system notifications, queue management, and first-class **Android Auto** & **Apple CarPlay** support -- all behind a single facade class and zero external state management dependencies. +A stream-based audio module for Flutter. Provides background playback, system notifications, queue management, and first-class **Android Auto** & **Apple CarPlay** support -- all behind a single facade class and zero external state management dependencies. + +This package reduces implementation overhead when combining packages such as `just_audio` and `audio_service`. It provides a simple wrapper API that captures our long-standing Flutter audio expertise in a single dependency.

Now Playing @@ -19,16 +21,16 @@ A beta, streams-based audio module for Flutter. Provides background playback, sy ## Features - **Background playback** with lock screen controls and media notifications -- **Queue management** -- add, insert, remove, reorder, shuffle +- **Queue management** - add, insert, remove, reorder, shuffle - **Seek forward / backward** with configurable intervals -- **Playback speed** control (0.5x -- 2.0x) -- **Repeat modes** -- off, one, all +- **Playback speed** control (0.5x - 2.0x) +- **Repeat modes** - off, one, all - **Live stream** support with ICY metadata - **Android Auto** integration via delegate pattern - **Apple CarPlay** integration with list, grid, and tab bar templates -- **Pre-built widgets** -- seek bar, play/pause, skip, speed selector, queue list, artwork, now playing info, and a full player builder -- **State management agnostic** -- pure Dart `BehaviorSubject` streams with synchronous getters -- **Audio session handling** -- automatic interruption and becoming-noisy management +- **Pre-built widgets** - seek bar, play/pause, skip, speed selector, queue list, artwork, now playing info, and a full player builder +- **State management agnostic** - `rxdart` `BehaviorSubject` streams with synchronous getters +- **Audio session handling** - automatic interruption and becoming-noisy management ## Core Stack @@ -37,7 +39,7 @@ A beta, streams-based audio module for Flutter. Provides background playback, sy | [just_audio](https://pub.dev/packages/just_audio) | ^0.10.5 | Audio playback engine | | [audio_service](https://pub.dev/packages/audio_service) | ^0.18.18 | Background playback & notifications | | [audio_session](https://pub.dev/packages/audio_session) | ^0.2.2 | Audio session & interruption handling | -| [mt_carplay](https://pub.dev/packages/mt_carplay) | custom fork | Apple CarPlay integration | +| [mt_carplay](https://pub.dev/packages/mt_carplay) | ^1.2.11 | Apple CarPlay integration (fork) | | [rxdart](https://pub.dev/packages/rxdart) | ^0.28.0 | Stream utilities & BehaviorSubjects | | [equatable](https://pub.dev/packages/equatable) | ^2.0.8 | Value equality for models | @@ -49,7 +51,7 @@ Add to your `pubspec.yaml`: ```yaml dependencies: - mt_audio: ^0.2.0-beta.1 + mt_audio: ^0.2.0-beta.3 ``` --- @@ -266,40 +268,32 @@ final player = await MtAudioPlayer.init( ### Set an audio source -`MtAudioSource` is a sealed class with three variants: - ```dart // Single track -await player.setSource( - MtSingleSource( - item: MtAudioItem( - id: '1', - uri: Uri.parse('https://example.com/audio.mp3'), - title: 'My Song', - artist: 'Artist Name', - artworkUri: Uri.parse('https://example.com/artwork.jpg'), - duration: Duration(minutes: 3, seconds: 45), - ), +await player.setAudioItem( + MtAudioItem( + id: '1', + uri: Uri.parse('https://example.com/audio.mp3'), + title: 'My Song', + artist: 'Artist Name', + artworkUri: Uri.parse('https://example.com/artwork.jpg'), + duration: Duration(minutes: 3, seconds: 45), ), ); // Playlist -await player.setSource( - MtPlaylistSource( - items: [item1, item2, item3], - initialIndex: 0, - ), +await player.setPlaylist( + [item1, item2, item3], + initialIndex: 0, ); -// Live stream -await player.setSource( - MtLiveSource( - item: MtAudioItem( - id: 'live', - uri: Uri.parse('https://example.com/stream'), - title: 'Live Radio', - isLive: true, - ), +// Live stream (isLive flag on the item controls live behavior) +await player.setAudioItem( + MtAudioItem( + id: 'live', + uri: Uri.parse('https://example.com/stream'), + title: 'Live Radio', + isLive: true, ), ); ``` @@ -550,7 +544,7 @@ class MyAndroidAutoDelegate implements MtAndroidAutoDelegate { @override Future onPlayFromMediaId(String mediaId) async { final track = await repository.getTrackById(mediaId); - await player.setSource(MtSingleSource(item: track)); + await player.setAudioItem(track); await player.play(); } @@ -634,7 +628,7 @@ class MyCarPlayDelegate extends MtCarPlayDelegate { @override Future onPlayFromMediaId(String mediaId) async { final track = await repository.getTrackById(mediaId); - await player.setSource(MtSingleSource(item: track)); + await player.setAudioItem(track); await player.play(); } } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index c42c8a1..82b59ab 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -94,7 +94,7 @@ class _HomePageState extends State { // Load source button FilledButton.icon( - onPressed: () => _loadSource(player), + onPressed: () => _loadAudioSource(player), icon: const Icon(Icons.play_circle_outline), label: Text( _sourceType == SourceType.playlist @@ -289,22 +289,19 @@ class _HomePageState extends State { ); } - Future _loadSource(MtAudioPlayer player) async { - MtAudioSource source; - + Future _loadAudioSource(MtAudioPlayer player) async { switch (_sourceType) { case SourceType.single: - source = MtSingleSource(item: sampleTracks[_selectedTrackIndex]); + await player.setAudioItem(sampleTracks[_selectedTrackIndex]); case SourceType.playlist: - source = MtPlaylistSource( - items: sampleTracks, + await player.setPlaylist( + sampleTracks, initialIndex: _selectedTrackIndex, ); case SourceType.live: - source = MtLiveSource(item: liveStreams[_selectedLiveIndex]); + await player.setAudioItem(liveStreams[_selectedLiveIndex]); } - await player.setSource(source); await player.play(); } } diff --git a/example/lib/pages/queue_page.dart b/example/lib/pages/queue_page.dart index 54cd914..9e0ca35 100644 --- a/example/lib/pages/queue_page.dart +++ b/example/lib/pages/queue_page.dart @@ -40,9 +40,7 @@ class QueuePage extends StatelessWidget { Expanded( child: FilledButton.icon( onPressed: () async { - await player.setSource( - MtPlaylistSource(items: sampleTracks), - ); + await player.setPlaylist(sampleTracks); await player.play(); }, icon: const Icon(Icons.playlist_play), diff --git a/example/lib/providers/example_android_auto_delegate.dart b/example/lib/providers/example_android_auto_delegate.dart index a3752e2..fe58e18 100644 --- a/example/lib/providers/example_android_auto_delegate.dart +++ b/example/lib/providers/example_android_auto_delegate.dart @@ -51,7 +51,7 @@ class ExampleAndroidAutoDelegate implements MtAndroidAutoDelegate { (i) => i.id == mediaId, orElse: () => throw ArgumentError('Unknown media ID: $mediaId'), ); - await player.setSource(MtSingleSource(item: item)); + await player.setAudioItem(item); await player.play(); } diff --git a/example/lib/providers/example_carplay_delegate.dart b/example/lib/providers/example_carplay_delegate.dart index 6a30dde..3b5bba9 100644 --- a/example/lib/providers/example_carplay_delegate.dart +++ b/example/lib/providers/example_carplay_delegate.dart @@ -202,15 +202,13 @@ class ExampleCarPlayDelegate implements MtCarPlayDelegate { if (playlist != null && playlist.length > 1) { // Queue playback for playlist items final initialIndex = playlist.indexWhere((i) => i.id == mediaId); - await player.setSource( - MtPlaylistSource( - items: playlist, - initialIndex: initialIndex >= 0 ? initialIndex : 0, - ), + await player.setPlaylist( + playlist, + initialIndex: initialIndex >= 0 ? initialIndex : 0, ); } else { // Single item playback - await player.setSource(MtSingleSource(item: item)); + await player.setAudioItem(item); } await player.play(); diff --git a/example/pubspec.lock b/example/pubspec.lock index 35325ad..2147bf2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -278,7 +278,7 @@ packages: path: ".." relative: true source: path - version: "0.1.0" + version: "0.2.0-beta.2" mt_carplay: dependency: transitive description: @@ -488,10 +488,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.9" typed_data: dependency: transitive description: diff --git a/lib/mt_audio.dart b/lib/mt_audio.dart index c71dc27..c6ee63b 100644 --- a/lib/mt_audio.dart +++ b/lib/mt_audio.dart @@ -21,7 +21,6 @@ export 'src/carplay/mt_carplay_item.dart'; // Models export 'src/models/mt_audio_error.dart'; export 'src/models/mt_audio_item.dart'; -export 'src/models/mt_audio_source.dart'; export 'src/models/mt_media_library_item.dart'; export 'src/models/mt_playback_state.dart'; export 'src/models/mt_position_state.dart'; diff --git a/lib/src/android_auto/mt_android_auto_delegate.dart b/lib/src/android_auto/mt_android_auto_delegate.dart index ad7f52d..52a3ce3 100644 --- a/lib/src/android_auto/mt_android_auto_delegate.dart +++ b/lib/src/android_auto/mt_android_auto_delegate.dart @@ -27,7 +27,7 @@ import 'package:mt_audio/src/models/mt_media_library_item.dart'; /// @override /// Future onPlayFromMediaId(String mediaId) async { /// final track = await getTrack(mediaId); -/// await player.setSource(MtSingleSource(item: track)); +/// await player.setAudioItem(track); /// await player.play(); /// } /// diff --git a/lib/src/carplay/mt_carplay_delegate.dart b/lib/src/carplay/mt_carplay_delegate.dart index 59b8081..37090e8 100644 --- a/lib/src/carplay/mt_carplay_delegate.dart +++ b/lib/src/carplay/mt_carplay_delegate.dart @@ -140,7 +140,7 @@ class MtCarPlaySection { /// @override /// Future onPlayFromMediaId(String mediaId) async { /// final track = await getTrack(mediaId); -/// await player.setSource(MtSingleSource(item: track)); +/// await player.setAudioItem(track); /// await player.play(); /// } /// } diff --git a/lib/src/handler/mt_audio_handler.dart b/lib/src/handler/mt_audio_handler.dart index 52f4b00..1aa79f2 100644 --- a/lib/src/handler/mt_audio_handler.dart +++ b/lib/src/handler/mt_audio_handler.dart @@ -17,7 +17,7 @@ class MtAudioHandler extends BaseAudioHandler MtAudioHandler({ Duration ffRewindInterval = const Duration(seconds: 10), }) : _ffRewindInterval = ffRewindInterval, - _lateBindingDelegate = LateBindingAndroidAutoDelegate() { + _androidAutoDelegate = LateBindingAndroidAutoDelegate() { _init(); } @@ -26,11 +26,17 @@ class MtAudioHandler extends BaseAudioHandler /// Fast-forward and rewind interval final Duration _ffRewindInterval; - final LateBindingAndroidAutoDelegate _lateBindingDelegate; + final LateBindingAndroidAutoDelegate _androidAutoDelegate; + + /// Whether queue synchronization is currently suppressed to avoid feedback loops + /// (e.g. flickering during reorder) bool _suppressQueueSync = false; final _errorSubject = BehaviorSubject.seeded(null); final _volumeSubject = BehaviorSubject.seeded(1); + + /// Allows for smoother seek bar updates by emitting optimistic position updates immediately on seek. + /// Note that this is NOT a 1:1 replacement for local seekbar state management, as it handles all other controls too. final _optimisticPositionSubject = BehaviorSubject.seeded(null); Duration? _optimisticPosition; @@ -65,6 +71,17 @@ class MtAudioHandler extends BaseAudioHandler _player.playbackEventStream.listen(_broadcastState); _player.positionStream.listen(_maybeClearOptimisticPosition); + // Forward playback errors to error subject + _player.errorStream.listen((error) { + _errorSubject.add( + MtAudioError( + code: MtAudioErrorCode.unknown, + message: error.message ?? 'Playback error', + details: error.toString(), + ), + ); + }); + // Initialize queue from player sequence _player.sequenceStateStream.listen((sequenceState) { if (_suppressQueueSync) return; @@ -129,6 +146,7 @@ class MtAudioHandler extends BaseAudioHandler return clamped; } + /// Sets an optimistic position that will be reflected in the playback state until the next actual position update from the player. void _setOptimisticPosition(Duration position) { _optimisticPosition = position; _optimisticPositionSubject.add(position); @@ -142,6 +160,7 @@ class MtAudioHandler extends BaseAudioHandler _broadcastState(_player.playbackEvent); } + /// Clears the optimistic position if the actual position from the player is close enough to it, indicating that the seek has been processed. void _maybeClearOptimisticPosition(Duration actualPosition) { final optimisticPosition = _optimisticPosition; if (optimisticPosition == null) return; @@ -197,26 +216,34 @@ class MtAudioHandler extends BaseAudioHandler } } - /// Sets the audio source from an [MtAudioSource]. - Future setAudioSource(MtAudioSource source) async { + /// Sets a single audio item as the source. + Future setItem(MtAudioItem item) async { try { - switch (source) { - case MtSingleSource(): - final audioSource = _createAudioSource(source.item); - await _player.setAudioSource(audioSource); - - case MtPlaylistSource(): - await _player.setAudioSources( - source.items.map(_createAudioSource).toList(), - initialIndex: source.initialIndex, - ); - - case MtLiveSource(): - final audioSource = _createAudioSource(source.item); - await _player.setAudioSource(audioSource); - } + final audioSource = _createAudioSource(item); + await _player.setAudioSource(audioSource); + _errorSubject.add(null); + } catch (e) { + final error = MtAudioError( + code: MtAudioErrorCode.sourceLoadFailed, + message: 'Failed to load audio source', + details: e.toString(), + ); + _errorSubject.add(error); + rethrow; + } + } - _errorSubject.add(null); // Clear any previous errors + /// Sets a playlist of audio items as the source. + Future setPlaylist( + List items, { + int initialIndex = 0, + }) async { + try { + await _player.setAudioSources( + items.map(_createAudioSource).toList(), + initialIndex: initialIndex, + ); + _errorSubject.add(null); } catch (e) { final error = MtAudioError( code: MtAudioErrorCode.sourceLoadFailed, @@ -500,14 +527,14 @@ class MtAudioHandler extends BaseAudioHandler /// This must be called after player creation but before any Android Auto /// browsing requests occur. void bindDelegate(MtAndroidAutoDelegate delegate) { - _lateBindingDelegate.bind(delegate); + _androidAutoDelegate.bind(delegate); } /// Whether the delegate has been bound. - bool get isDelegateBound => _lateBindingDelegate.isBound; + bool get isDelegateBound => _androidAutoDelegate.isBound; @override - MtAndroidAutoDelegate get androidAutoDelegate => _lateBindingDelegate; + MtAndroidAutoDelegate get androidAutoDelegate => _androidAutoDelegate; /// Disposes of this handler and releases resources. Future dispose() async { diff --git a/lib/src/models/mt_audio_source.dart b/lib/src/models/mt_audio_source.dart deleted file mode 100644 index 32a3bbf..0000000 --- a/lib/src/models/mt_audio_source.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:mt_audio/src/models/mt_audio_item.dart'; - -/// Base class for audio sources. -/// -/// Represents different types of audio sources that can be played. -/// Use one of the concrete implementations: -/// - [MtSingleSource] for a single audio track -/// - [MtPlaylistSource] for a playlist of tracks -/// - [MtLiveSource] for a live audio stream -sealed class MtAudioSource extends Equatable { - const MtAudioSource(); -} - -/// A source representing a single audio track. -class MtSingleSource extends MtAudioSource { - /// Creates a [MtSingleSource]. - const MtSingleSource({ - required this.item, - }); - - /// The audio item to play. - final MtAudioItem item; - - @override - List get props => [item]; - - @override - String toString() => 'MtSingleSource(item: $item)'; -} - -/// A source representing a playlist of audio tracks. -class MtPlaylistSource extends MtAudioSource { - /// Creates a [MtPlaylistSource]. - const MtPlaylistSource({ - required this.items, - this.initialIndex = 0, - }); - - /// List of audio items in the playlist. - final List items; - - /// Index of the initial item to play (defaults to 0). - final int initialIndex; - - @override - List get props => [items, initialIndex]; - - @override - String toString() => - 'MtPlaylistSource(items: ${items.length}, initialIndex: $initialIndex)'; -} - -/// A source representing a live audio stream. -class MtLiveSource extends MtAudioSource { - /// Creates a [MtLiveSource]. - const MtLiveSource({ - required this.item, - }); - - /// The live stream item. - /// - /// The item should have [MtAudioItem.isLive] set to true. - final MtAudioItem item; - - @override - List get props => [item]; - - @override - String toString() => 'MtLiveSource(item: $item)'; -} diff --git a/lib/src/player/mt_audio_player.dart b/lib/src/player/mt_audio_player.dart index 02c910f..63d37e7 100644 --- a/lib/src/player/mt_audio_player.dart +++ b/lib/src/player/mt_audio_player.dart @@ -7,7 +7,6 @@ import 'package:mt_audio/src/carplay/mt_carplay_handler.dart'; import 'package:mt_audio/src/handler/mt_audio_handler.dart'; import 'package:mt_audio/src/models/mt_audio_error.dart'; import 'package:mt_audio/src/models/mt_audio_item.dart'; -import 'package:mt_audio/src/models/mt_audio_source.dart'; import 'package:mt_audio/src/models/mt_playback_state.dart'; import 'package:mt_audio/src/models/mt_position_state.dart'; import 'package:mt_audio/src/models/mt_queue_state.dart'; @@ -30,12 +29,10 @@ import 'package:rxdart/rxdart.dart'; /// ); /// /// // Set audio source -/// await player.setSource(MtSingleSource( -/// item: MtAudioItem( -/// id: '1', -/// uri: Uri.parse('https://example.com/audio.mp3'), -/// title: 'My Audio', -/// ), +/// await player.setAudioItem(MtAudioItem( +/// id: '1', +/// uri: Uri.parse('https://example.com/audio.mp3'), +/// title: 'My Audio', /// )); /// /// // Listen to state @@ -359,9 +356,12 @@ class MtAudioPlayer { //* Queue management - /// Sets the audio source and replaces the current queue. - Future setSource(MtAudioSource source) => - _handler.setAudioSource(source); + /// Sets a single audio item as the source and replaces the current queue. + Future setAudioItem(MtAudioItem item) => _handler.setItem(item); + + /// Sets a playlist as the source and replaces the current queue. + Future setPlaylist(List items, {int initialIndex = 0}) => + _handler.setPlaylist(items, initialIndex: initialIndex); /// Adds an item to the end of the queue. Future addToQueue(MtAudioItem item) => _handler.addAudioItem(item); diff --git a/pubspec.lock b/pubspec.lock index a752a60..3466e09 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -481,10 +481,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.9" typed_data: dependency: transitive description: From 7a24d66a05dc81c4ef748f0db4c451b736100b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wyczarski?= Date: Mon, 16 Feb 2026 14:17:36 +0100 Subject: [PATCH 2/3] Forwards rewind/fast-forward interval to system Ensures the configured rewind and fast-forward intervals are correctly used by system media controls. Simplifies the internal playback state stream and prevents redundant emissions for improved efficiency. --- CHANGELOG.md | 5 +++ lib/src/player/mt_audio_player.dart | 52 +++++++++++------------------ 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dbb67a..7f324e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ### Changed - Replaced `MtAudioSource` sealed class (`MtSingleSource`, `MtPlaylistSource`, `MtLiveSource`) with two explicit methods on `MtAudioPlayer`: `setAudioItem(MtAudioItem)` and `setPlaylist(List, {int initialIndex})`. +- Simplified internal playback state stream and added `.distinct()` to prevent redundant emissions. + +### Fixed + +- `ffRewindInterval` from `MtAudioPlayerConfig` is now correctly forwarded to system media controls (lock screen, notification, Android Auto, CarPlay). Previously, system-level fast-forward/rewind buttons used default intervals instead of the configured value. ### Breaking changes diff --git a/lib/src/player/mt_audio_player.dart b/lib/src/player/mt_audio_player.dart index 63d37e7..11ffec4 100644 --- a/lib/src/player/mt_audio_player.dart +++ b/lib/src/player/mt_audio_player.dart @@ -78,6 +78,8 @@ class MtAudioPlayer { androidShowNotificationBadge: true, preloadArtwork: true, androidNotificationOngoing: true, + fastForwardInterval: config.ffRewindInterval, + rewindInterval: config.ffRewindInterval, ), ); @@ -160,39 +162,21 @@ class MtAudioPlayer { ); _subscriptions.add( - Rx.combineLatest2< - ({ - AudioProcessingState processingState, - bool playing, - AudioServiceRepeatMode repeatMode, - AudioServiceShuffleMode shuffleMode, - double speed, - }), - double, - void - >( - playbackUiStateStream, - _handler.volumeStream, - (playbackState, volume) { - final status = _mapAudioProcessingState( - playbackState.processingState, - playbackState.playing, - ); - final repeatMode = _mapRepeatMode(playbackState.repeatMode); - - _playbackStateSubject.add( - MtPlaybackState( - status: status, - repeatMode: repeatMode, - shuffleEnabled: - playbackState.shuffleMode == AudioServiceShuffleMode.all, - volume: volume, - speed: playbackState.speed, - ), - ); - }, - ) - .listen((_) {}), + Rx.combineLatest2( + playbackUiStateStream, + _handler.volumeStream, + (playbackState, volume) => MtPlaybackState( + status: _mapAudioProcessingState( + playbackState.processingState, + playbackState.playing, + ), + repeatMode: _mapRepeatMode(playbackState.repeatMode), + shuffleEnabled: + playbackState.shuffleMode == AudioServiceShuffleMode.all, + volume: volume, + speed: playbackState.speed, + ), + ).distinct().listen(_playbackStateSubject.add), ); // Position state stream @@ -405,6 +389,7 @@ class MtAudioPlayer { MtRepeatMode.one => AudioServiceRepeatMode.one, MtRepeatMode.all => AudioServiceRepeatMode.all, }; + return _handler.setRepeatMode(audioServiceMode); } @@ -413,6 +398,7 @@ class MtAudioPlayer { final audioServiceMode = enabled ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none; + return _handler.setShuffleMode(audioServiceMode); } From 165090c84dd4418249b7106025c87708771320f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wyczarski?= Date: Mon, 16 Feb 2026 14:40:37 +0100 Subject: [PATCH 3/3] Bumps version to 0.2.0-beta.3 Updates the pubspec version to 0.2.0-beta.3 and adjusts the tag matching regular expression in the publish workflow to allow for multi-digit version numbers. --- .github/workflows/publish.yml | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d928631..106b832 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,8 +3,8 @@ name: Publish to pub.dev on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-**" + - "v[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*" + - "v[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*-*" permissions: contents: read diff --git a/pubspec.yaml b/pubspec.yaml index 93be095..cb327f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: mt_audio -version: 0.2.0-beta.2 +version: 0.2.0-beta.3 description: A beta, streams-based Flutter audio package with background playback, queue management, Android Auto, and Apple CarPlay support. homepage: https://github.com/mobitouchOS/mt_audio repository: https://github.com/mobitouchOS/mt_audio