From 44cbd4532b01ea1a55fdc384102d3e67da574cd2 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 05:13:20 +0700 Subject: [PATCH] docs(third-party): complete the vendored plugin patch notes Complete the audio_service and audioplayers_android notes and add one for audioplayers_darwin: how each copy is wired into pubspec.yaml, every patch, the changed files, and an upgrade procedure. Each list was checked with diff -ru against the matching pub.dev package in the local pub cache (audio_service 0.18.19, audioplayers_android 5.3.0, audioplayers_darwin 6.5.0). No plugin code changed. Derived from those diffs, pubspec.yaml, the plugins' Gradle and podspec files, and the Dart callers lib/services/music_player_service.dart and lib/services/music_playback_deck.dart. --- .../audio_service/SPOTIFLAC_PATCHES.md | 68 ++++++++++++++- .../audioplayers_android/SPOTIFLAC_PATCHES.md | 77 +++++++++++++++++ .../audioplayers_darwin/SPOTIFLAC_PATCHES.md | 85 +++++++++++++++++++ 3 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 third_party/audioplayers_darwin/SPOTIFLAC_PATCHES.md diff --git a/third_party/audio_service/SPOTIFLAC_PATCHES.md b/third_party/audio_service/SPOTIFLAC_PATCHES.md index 9f28fabf..5ad5d4bc 100644 --- a/third_party/audio_service/SPOTIFLAC_PATCHES.md +++ b/third_party/audio_service/SPOTIFLAC_PATCHES.md @@ -4,26 +4,90 @@ Source: the published `audio_service` 0.18.19 package (MIT; see LICENSE). Only runtime sources are vendored. This replaces the same dependency, without adding another playback engine or native SDK. +The root `pubspec.yaml` uses this copy as a path dependency +(`audio_service: path: third_party/audio_service`), so the version is the one +in this folder's `pubspec.yaml` (0.18.19, unchanged from upstream). +`example/`, `test/`, `analysis_options.yaml` and `darwin/audio_service/.swiftpm` +from the upstream package are not vendored. + +## Patches + - Android: include custom media actions in MediaStyle notifications on Android 12 and older. Upstream exposes them only through PlaybackState, which hides them on those versions. Keep custom action order and compact indices aligned. + AudioService.java adds each custom control as a notification action with + its own PendingIntent. Without an activity class, the intent is a broadcast + to `MediaButtonReceiver` with the new `ACTION_CUSTOM` + (`com.ryanheise.audioservice.intent.action.CUSTOM`), which calls + `AudioService.handleCustomNotificationAction` and so the session's + `onCustomAction`. - Android: allow a custom action to supply an explicit in-app activity class using the `spotiflac.activity` extra. Its notification PendingIntent opens the output picker directly. These activity controls are excluded from PlaybackState custom actions: background launches from media-session binder callbacks are blocked on newer Android. Use its native output switcher. - This is only for app-owned controls, not extension input. + This is only for app-owned controls, not extension input. No app code + (Dart, Kotlin or Swift) currently sets `spotiflac.activity`. - Android: bound retained browser metadata to 256 recent items with `PinnedMetadataCache`. The active queue and current item stay pinned so notification, Android Auto and browser lookups survive recent-item eviction. Replacing the queue and clearing the service release their pinned metadata. + To support eviction, the `ServiceListener` queue and play-item callbacks + take a `MediaDescriptionCompat` instead of `MediaMetadataCompat`. + AudioServicePlugin.java resolves the metadata from the cache, then from + the description extras (browsed items carry it under + `com.ryanheise.audio_service.metadata`), then from Dart `getMediaItem`. + After `onDestroy`, metadata writes throw instead of repopulating the cache. + PinnedMetadataCacheTest.java (JUnit, under `android/src/test`) covers it. +- Android: `setMediaItem` and `setQueue` run on one serial `mediaWorker` + executor per handler interface, instead of a new single-thread executor per + call. Each call captures the current `AudioService` and fails with "Audio + service is no longer attached" when the service changed or the interface + was destroyed; `destroy()` also clears the method handler and shuts the + executor down. +- Android build (`android/build.gradle.kts`): AGP 9.0.1 to 9.4.1, compileSdk + 35 to 37, `androidx.media:media` 1.7.0 to 1.8.0, `androidx.core:core` + 1.13.1 to 1.19.1, a `junit:junit:4.13.2` test dependency, and + `-Xlint:deprecation` dropped (androidx.media 1.8 deprecates the whole compat + media-session API this plugin uses). The Gradle wrapper moves from 8.7 to + 9.8.1. These follow the app's Android toolchain. - iOS: allow the active music handler to re-publish retained Now Playing metadata after restoring its audio session on app lifecycle/video changes. Refresh keeps artwork and extrapolates elapsed time without restarting audio; it never activates the audio session or revives a stopped service. + `lib/audio_service.dart` adds `AudioService.refreshNowPlayingInfo()`, which + calls the new `refreshNowPlayingInfo` method in AudioServicePlugin.m; + `lib/services/music_player_service.dart` in the app calls it. Android 13+ still chooses the positions and built-in transport icons in System UI. We preserve real play/pause/previous/next semantics for headsets, lock screen and Android Auto instead of disguising them as unrelated custom actions. -When upgrading, compare these changes against the matching upstream files. +## Changed Files + +Compared against the pub.dev 0.18.19 package in the local pub cache: + +- `android/build.gradle.kts`, `android/gradle/wrapper/gradle-wrapper.properties` +- `android/src/main/java/com/ryanheise/audioservice/AudioService.java` +- `android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java` +- `android/src/main/java/com/ryanheise/audioservice/MediaButtonReceiver.java` +- `android/src/main/java/com/ryanheise/audioservice/PinnedMetadataCache.java` (new) +- `android/src/test/java/com/ryanheise/audioservice/PinnedMetadataCacheTest.java` (new) +- `darwin/audio_service/Sources/audio_service/AudioServicePlugin.m` +- `lib/audio_service.dart` + +## Upgrading + +1. Fetch both versions: `dart pub cache add audio_service --version 0.18.19` + and the same for the new version. +2. Record the local patch: + `diff -ru ~/.pub-cache/hosted/pub.dev/audio_service-0.18.19 third_party/audio_service`. +3. Replace the runtime sources with the new upstream files, then re-apply + each patch above by hand. Check first whether upstream now shows custom + actions on Android 12 and older, bounds its metadata cache, or reuses its + media executor. +4. Keep the Gradle values in step with the app's `android/app/build.gradle.kts`. +5. Test notification controls on Android 12 or older and on Android 13+, + Android Auto browsing, and iOS lock screen controls after a video plays. +6. Run `flutter pub get` so the root `pubspec.lock` records the new version, + and update this file. diff --git a/third_party/audioplayers_android/SPOTIFLAC_PATCHES.md b/third_party/audioplayers_android/SPOTIFLAC_PATCHES.md index 0be94fd6..f74fb8ed 100644 --- a/third_party/audioplayers_android/SPOTIFLAC_PATCHES.md +++ b/third_party/audioplayers_android/SPOTIFLAC_PATCHES.md @@ -2,8 +2,85 @@ Based on audioplayers_android 5.3.0 (license retained in LICENSE). +The root `pubspec.yaml` overrides the federated package with this copy +(`dependency_overrides: audioplayers_android: path: +third_party/audioplayers_android`), next to `audioplayers: ^6.8.1`. The +version is the one in this folder's `pubspec.yaml` (5.3.0, unchanged). + +## Patches + +### Proc descriptor sources + `UrlSource` duplicates app-owned `/proc/self/fd/` sources and passes the descriptor directly to MediaPlayer. Android AppFuse proxies, including remote SAF providers, permit only the original open; reopening the proc path fails. The caller must hold the original lease until source preparation completes. Other URLs and file paths retain upstream behavior. + +Only local sources (`isLocal`) with a non-negative descriptor number take +this path. The app opens these leases with `safOpenPlaybackLease` (see +`android/README.md` in the app). + +### Per-player equalizer + +A `setEqualizer` method (argument `configuration`) on the per-player method +channel, handled in AudioplayersPlugin.kt, WrappedPlayer.kt and +MediaPlayerWrapper.kt, with the new player/PlaybackEqualizer.kt. On +Android 9 (API 28) and later it attaches a `DynamicsProcessing` effect to the +player's own nonzero audio session; it never attaches to session zero. It +accepts 5, 10, 15 or 31 bands with gains from -24 to 24 dB. Below API 28, +enabling it fails and disabling it is a no-op. The effect is closed on +`reset` and `release`. `README.md` describes the user-facing behavior; the +Dart caller is `lib/services/music_playback_deck.dart` in the app. + +### Rate and start + +- `setRate(1.0)` no longer calls `setPlaybackParams` until a different rate + was applied, so normal playback avoids the time-stretch path. After a + change, 1.0 is applied again to restore the original speed. +- `start()` always calls `MediaPlayer.start()` after `setRate`, instead of + relying on `setPlaybackParams` (or, before API 23, `setRate(1.0)`) to start + output. + +### Error state + +`WrappedPlayer.onError` now marks the player as not prepared and not playing, +releases audio focus, reports "Android could not open this audio source." or +"Android could not play this audio." (depending on whether the source was +prepared) with the `what` and `extra` codes as details, and returns `true`. +Upstream returns `false`, which makes MediaPlayer also call the completion +listener, so the app's queue advanced after a playback failure. + +### Build + +`android/build.gradle`: Kotlin 1.7.10 to 2.4.21, coroutines 1.6.4 to 1.11.0, +AGP 7.3.1 to 9.4.1, android-junit5 1.7.1.1 to 2.0.1, compileSdk 36 to 37, +`androidx.core:core-ktx` 1.9.0 to 1.19.1, `kotlin-stdlib-jdk7` replaced by +`kotlin-stdlib`, and newer JUnit Jupiter and AssertJ test dependencies. +`CHANGELOG.md` differs only by removed trailing whitespace. + +## Changed Files + +Compared against the pub.dev 5.3.0 package in the local pub cache: + +- `android/build.gradle` +- `android/src/main/kotlin/xyz/luan/audioplayers/AudioplayersPlugin.kt` +- `android/src/main/kotlin/xyz/luan/audioplayers/player/MediaPlayerWrapper.kt` +- `android/src/main/kotlin/xyz/luan/audioplayers/player/PlaybackEqualizer.kt` (new) +- `android/src/main/kotlin/xyz/luan/audioplayers/player/WrappedPlayer.kt` +- `android/src/main/kotlin/xyz/luan/audioplayers/source/UrlSource.kt` +- `README.md` (the "SpotiFLAC local changes" section), `CHANGELOG.md` + +## Upgrading + +1. Fetch both versions: `dart pub cache add audioplayers_android --version 5.3.0` + and the same for the new version. +2. Record the local patch: + `diff -ru ~/.pub-cache/hosted/pub.dev/audioplayers_android-5.3.0 third_party/audioplayers_android`. +3. Replace the sources with the new upstream files and re-apply each patch + above. Check whether upstream now handles proc descriptors, the error + return value, or starting after `setPlaybackParams`. +4. Keep the Gradle values in step with the app's `android/app/build.gradle.kts`. +5. Test playback from a remote SAF provider, the equalizer, a playback error + in the middle of a queue, and rate changes back to 1.0. +6. Run `flutter pub get` and update this file. diff --git a/third_party/audioplayers_darwin/SPOTIFLAC_PATCHES.md b/third_party/audioplayers_darwin/SPOTIFLAC_PATCHES.md new file mode 100644 index 00000000..b5a1de81 --- /dev/null +++ b/third_party/audioplayers_darwin/SPOTIFLAC_PATCHES.md @@ -0,0 +1,85 @@ +# Local audioplayers_darwin patch + +Based on `audioplayers_darwin` 6.5.0 from bluefireteam/audioplayers (MIT; +license retained in LICENSE). `README.md` describes the user-facing +behavior; this file lists every difference from upstream. + +The root `pubspec.yaml` overrides the federated package with this copy +(`dependency_overrides: audioplayers_darwin: path: +third_party/audioplayers_darwin`), next to `audioplayers: ^6.8.1`. The +version is the one in this folder's `pubspec.yaml` (6.5.0, unchanged). +`CHANGELOG.md`, `analysis_options.yaml` and +`darwin/audioplayers_darwin/.swiftpm` from the upstream package are not +vendored. + +## Patches + +### Per-player equalizer + +- AudioplayersDarwinPlugin.swift handles a `setEqualizer` method with a + `configuration` map. A missing map or a failed attach returns a + `DarwinAudioError`. +- WrappedMediaPlayer.swift keeps the configuration per player and attaches + the tap to the current `AVPlayerItem` when the equalizer is enabled. Each + new source gets a fresh tap so filter state is never shared between items. +- EqualizerAudioTap.swift (new) installs an `MTAudioProcessingTap` through + an `AVMutableAudioMix` on the item. Protected assets and `.m3u8` URLs fail + with "Equalizer is unavailable for protected audio or HLS streams". +- EqualizerDSP.swift (new) processes decoded PCM with cascaded RBJ peaking + biquads, plus input gain, balance, a compressor and a limiter. It accepts 5, 10, + 15 or 31 bands with gains from -24 to 24 dB, the same limits as the + Android fork. + +### External playback + +`WrappedMediaPlayer` sets `player.allowsExternalPlayback = false`. Audio then +follows the system route (AirPlay audio) through the tap, instead of handing +the item to the receiver as an external video session that bypasses the +equalizer. + +### Packaging + +- `darwin/audioplayers_darwin.podspec` adds + `s.frameworks = 'AVFoundation', 'MediaToolbox'` for the tap. +- `pubspec.yaml` drops `resolution: workspace`; this copy is not a member of + the upstream pub workspace. +- `pubspec.lock` in this folder is tracked but not part of upstream. + +## Tests + +`tests/EqualizerDSPTests.swift` is a standalone `@main` program for the DSP. +No script or CI job runs it. On macOS: + +```bash +cd third_party/audioplayers_darwin +swiftc darwin/audioplayers_darwin/Sources/audioplayers_darwin/EqualizerDSP.swift \ + tests/EqualizerDSPTests.swift -o /tmp/eq_dsp_tests && /tmp/eq_dsp_tests +``` + +Build without `-O` so its `assert` checks stay active. It prints a `PASS` +line on success. + +## Changed Files + +Compared against the pub.dev 6.5.0 package in the local pub cache: + +- `darwin/audioplayers_darwin.podspec` +- `darwin/audioplayers_darwin/Sources/audioplayers_darwin/AudioplayersDarwinPlugin.swift` +- `darwin/audioplayers_darwin/Sources/audioplayers_darwin/WrappedMediaPlayer.swift` +- `darwin/audioplayers_darwin/Sources/audioplayers_darwin/EqualizerAudioTap.swift` (new) +- `darwin/audioplayers_darwin/Sources/audioplayers_darwin/EqualizerDSP.swift` (new) +- `tests/EqualizerDSPTests.swift` (new) +- `pubspec.yaml`, `README.md`, `pubspec.lock` (new) + +## Upgrading + +1. Fetch both versions: `dart pub cache add audioplayers_darwin --version 6.5.0` + and the same for the new version. +2. Record the local patch: + `diff -ru ~/.pub-cache/hosted/pub.dev/audioplayers_darwin-6.5.0 third_party/audioplayers_darwin`. +3. Replace the sources with the new upstream files and re-apply each patch + above. Keep the `setEqualizer` arguments in step with the Android fork and + with `lib/services/music_playback_deck.dart` in the app. +4. Run the DSP test above, then test the equalizer, source changes and + AirPlay audio on a device. +5. Run `flutter pub get` and `(cd ios && pod install)`, then update this file.