A Flutter plugin for iOS and Android which surfaces metadata around the currently playing audio track on the device.
On Android nowplaying
makes use of the NotifiationListenerService
, and shows any
track revealing its play state via a notification.
On iOS nowplaying
is restricted to access to music or media played via the Apple Music/iTunes app,
or via Spotify.
Access to Spotify track listings is dependent on the user signing in to Spotify and authorising the app for its use.
Add nowplaying
as a dependency in your pubspec.yaml
file:
dependencies:
nowplaying: ^3.0.3
Add the following usage to your ios/Runner/Info.plist
:
<key>NSAppleMusicUsageDescription</key>
<string>We need this to show you what's currently playing</string>
To enable the notification listener service, add the following block to your android/app/src/main/AndroidManifest.xml
, just before the closing </application>
tag:
<service android:name="com.gomes.nowplaying.NowPlayingListenerService"
android:label="NowPlayingListenerService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
As stated in https://developer.android.com/preview/privacy/package-visibility:
Android 11 changes how apps can query and interact with other apps that the user has installed on a device. Using the new element, apps can define the set of other apps that they can access. This element helps encourage the principle of least privilege by telling the system which other apps to make visible to your app, and it helps app stores like Google Play assess the privacy and security that your app provides for users.
If your app targets Android 11, you might need to add the element in your app's manifest file. Within the element, you can specify apps by package name or by intent signature.
So you either have to stop what you are doing, or request to access information about certain
packages, or - if you have reasons for it - use the permission [QUERY_ALL_PACKAGES
][https://developer.android.com/reference/kotlin/android/Manifest.permission#query_all_packages].
To query and interact with specific packages you would update your AndroidManifest.xml
like this:
<manifest ...>
...
<queries>
<package android:name="com.example.store" />
<package android:name="com.example.services" />
</queries>
...
<application ...>
...
</manifest>
Have an app that needs to be able to ask for information for all apps? Add the following to AndroidManifest.xml
:
<manifest ...>
...
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
...
<application ...>
...
</manifest>
Note: For use queries you should write the queries code out of the application tag, not inside the application tag
You'll also need to update the gradle version to a proper version, which supports Android 11 (if you don't have it already)
- https://android-developers.googleblog.com/2020/07/preparing-your-build-for-package-visibility-in-android-11.html
- https://stackoverflow.com/questions/62969917/how-to-fix-unexpected-element-queries-found-in-manifest-error/66851218#66851218
Initialise the nowplaying
service by starting its instance:
await NowPlaying.instance.start();
This can be done anywhere, including prior to the runApp
command.
iOS automatically has the required permissions to access now-playing data, via the usage key added during the installation phase.
Android users must give explicit permission for the service to access the notification stream from which now-playing data is extracted.
Test for whether permissions have been given or not via the instance's isEnabled
method:
final bool isEnabled = await NowPlaying.instance.isEnabled();
// isEnabled() always returns true on iOS
if (!isEnabled) {
...
}
The Android settings page for this permission is a little hard to find, so NowPlaying includes a convenience method to open it:
NowPlaying.instance.requestPermissions();
To avoid annoying a user by e.g. showing the permissions page on every app restart, navigation to this page should be limited: as such, the unparameterised requestPermissions
function will only open the settings page once for any given install of the app. It returns a boolean: true
the first time, when the page has been successfully shown; also true
if permission has already been granted (in which case the settings page is not shown); or false
if this is a second or later call to the method, with navigation to the settings page prohibited. (Note that requestPermissions()
always returns true
on iOS).
final bool hasShownPermissions = await NowPlaying.instance.requestPermissions();
If you really need to show the permissions page a second time, probably after gently explaining to the user why, you can force
it open:
if (!hasShownPermissions) {
final bool pleasePleasePlease = await Navigator.of(context).pushNamed('ExplainAgainReallyNicelyPage');
if (pleasePleasePlease) NowPlaying.instance.requestPermissions(force: true);
}
(although this still won't show the settings page if permission is already enabled.)
Access to spotify requires a client ID and client secret, available from the Spotify Developer Dashboard.
Once you have these credentials, pass them into the NowPlaying start
function:
await NowPlaying.instance.start(
...,
spotifyClientId: '<CLIENT ID>',
spotifyClientSecret: '<CLIENT SECRET>',
...
);
Once credentials have been provided, you need to connect to spotify:
if (NowPlaying.spotify.isEnabled && NowPlaying.spotify.isUnconnected) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => NowPlaying.spotify.signInPage(context)),
);
}
If required, this will bring up a Spotify sign-in page in a webview.
Now-playing metadata is deliverd into the parent app via a stream
of NowPlayingTrack
objects, exposed as NowPlaying.instance.stream
. This can be consumed however you'd usually consume a stream, e.g.:
StreamProvider.value(
value: NowPlaying.instance.stream,
child: MaterialApp(
home: Scaffold(
body: Consumer<NowPlayingTrack>(
builder: (context, track, _) {
return Container(
...
);
}
)
)
)
)
The NowPlayingTrack
objects contain the following fields:
String title;
String artist;
String album;
String genre;
Duration duration;
Duration progress; // check note below
NowPlayingState state;
ImageProvider image;
ImageProvider icon;
String source;
where NowPlayingState
is defined as:
enum NowPlayingState {
playing, paused, stopped
}
...which is hopefully self-explanatory.
The source
of a track is the package name of the app playing the current track: com.spotify.music
, for example. On iOS this is always com.apple.music
or 'Spotify'
The icon
image provider, if not null, supplies a small, transparent PNG containing a monochrome logo for the originating app. While monochrome, this PNG is not necessarily black: so for consistency, it's probably worth adding color: Colors.somethingNice
and colorBlendMode: BlendMode.srcIn
or similar to any Image
widget.
As is probably obvious, progress
is a duration describing how far through the track the player has progressed, in milliseconds: how much of a track has been played, in other words.
Note that no new track is emitted on the stream as a track progresses: stream updates only happen when the track changes state (playing to paused; vice versa; new track starts; and so on). However, the progress
field of a track will give you an instantaneous 'correct' value every time it's polled, so to see progress updating in real time create a stateful widge to expose it:
class TrackProgressIndicator extends StatefulWidget {
final NowPlayingTrack track;
TrackProgressIndicator(this.track);
@override
_TrackProgressIndicatorState createState() => _TrackProgressIndicatorState();
}
class _TrackProgressIndicatorState extends State<TrackProgressIndicator> {
Timer _timer;
@override
void initState() {
_timer = Timer.periodic(const Duration(seconds: 1), (_) => setState(() {}));
super.initState();
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(widget.track.progress.toString().split('.').first.padLeft(8, '0'));
}
}
Usually - and almost always, on Android - a track will contain an appropriate ImageProvider
in its image
field, containing album art or similar.
On iOS, however, there is a bug or badly documented policy that means album art is only made available if the track being played is in your local library: any tracks streamed from e.g. Apple music playlists are image-free.
NowPlaying
can attempt to resolve missing images for you. However, this is a relatively heavy process in terms of memory and processing, so is turned off by default. To enable missing image resolution, set the resolveImages
parameter to true
when starting the instance:
await NowPlaying.instance.start(..., resolveImages: true, ...);
The default image resolution process:
- will only attempt to find an image if none already exists
- access the Spotify api for image data if signed in, or
- makes http calls against the MusicBrainz api and subsequently the Cover Art Archive api
You may decide that you want to resolve missing images in a different way, or even override images that have already been found from the metadata. In this case, supply a new image resolver when starting the instance:
await NowPlaying.instance.start(..., resolver: MyImageResolver(), ...);
...
class MyImageResolver implements NowPlayingImageResolver {
@override
Future<ImageProvider?> resolve(NowPlayingTrack track) async {
...
}
}
- patch:
- bugfix, tweak or typo
- minor:
- non-breaking change
- major:
- breaking change
Thanks to Fábio A. M. Pereira for his Notification Listener Service Example, which provided inspiration (and in some cases, let's be honest, actual code) for the Android implementation.