Files
SpotiFLAC-Mobile/third_party/audio_service/lib/audio_service.dart
T
zarzet 5259c86365 feat(player): add Mornye media notification controls
Sync notification favorites with Library, add Mornye transport icons and an output action on legacy Android, and expose the iOS favorite command. Preserve playback position when controls refresh.

Vendor audio_service 0.18.19 with a small Android patch for custom notification actions. Modern Android keeps its system output switcher and transport layout.
2026-09-26 03:07:38 +07:00

4061 lines
136 KiB
Dart
Vendored

// ignore_for_file: close_sinks
import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'package:audio_service_platform_interface/audio_service_platform_interface.dart';
import 'package:audio_session/audio_session.dart';
import 'package:clock/clock.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:rxdart/rxdart.dart';
AudioServicePlatform _platform = AudioServicePlatform.instance;
/// The buttons on a headset.
enum MediaButton {
/// The "media" button on Android, or the play/pause button on iOS.
media,
/// The "skip to next" button.
next,
/// The "skip to previous" button.
previous,
}
/// The actions associated with playing audio.
enum MediaAction {
/// Stop playing audio.
stop,
/// Pause audio.
pause,
/// Play or resume audio.
play,
/// Rewind.
rewind,
/// Skip to the previous media item.
skipToPrevious,
/// Skip to the next media item.
skipToNext,
/// Fast forward.
fastForward,
/// Set a rating for the current media item.
setRating,
/// Seek within the current media item.
seek,
/// Toggle between playing and paused.
playPause,
/// Play a given media item by ID.
playFromMediaId,
/// Play media from a search.
playFromSearch,
/// Skip to a queue item.
skipToQueueItem,
/// Play media from a URI.
playFromUri,
/// Prepare media for playback.
prepare,
/// Prepare media for playback by ID.
prepareFromMediaId,
/// Prepare media for playback from a search.
prepareFromSearch,
/// Prepare media for playback from a URI.
prepareFromUri,
/// Set the repeat mode.
setRepeatMode,
/// Was depreceated in Android.
// ignore: unused_field
_setShuffleModeEnabled,
/// Set captioning enabled.
setCaptioningEnabled,
/// Set the shuffle mode.
setShuffleMode,
/// Seek backwards continuously.
seekBackward,
/// Seek forwards continuously.
seekForward,
/// Set speed.
setSpeed,
/// Custom MediaAction.
custom,
}
/// The states of audio processing.
enum AudioProcessingState {
/// There hasn't been any resource loaded yet.
idle,
/// Resource is being loaded.
loading,
/// Resource is being buffered.
buffering,
/// Resource is buffered enough and available for playback.
ready,
/// The end of resource was reached.
completed,
/// There was an error loading resource.
///
/// [PlaybackState.errorCode] and [PlaybackState.errorMessage] will be not null
/// in this state.
error,
}
/// The playback state which includes a [playing] boolean state, a processing
/// state such as [AudioProcessingState.buffering], the playback position and
/// the currently enabled actions to be shown in the Android notification or the
/// iOS control center.
class PlaybackState {
/// The audio processing state e.g. [AudioProcessingState.buffering].
final AudioProcessingState processingState;
/// Whether audio is either playing, or will play as soon as [processingState]
/// is [AudioProcessingState.ready]. A true value should be broadcast whenever
/// it would be appropriate for UIs to display a pause or stop button.
///
/// Since [playing] and [processingState] can vary independently, it is
/// possible distinguish a particular audio processing state while audio is
/// playing vs paused. For example, when buffering occurs during a seek, the
/// [processingState] can be [AudioProcessingState.buffering], but alongside
/// that [playing] can be true to indicate that the seek was performed while
/// playing, or false to indicate that the seek was performed while paused.
final bool playing;
/// The list of currently enabled controls which should be shown in the media
/// notification. Each control represents a clickable button with a
/// [MediaAction] that must be one of:
///
/// * [MediaAction.stop]
/// * [MediaAction.pause]
/// * [MediaAction.play]
/// * [MediaAction.rewind]
/// * [MediaAction.skipToPrevious]
/// * [MediaAction.skipToNext]
/// * [MediaAction.fastForward]
/// * [MediaAction.playPause]
final List<MediaControl> controls;
/// Up to 3 indices of the [controls] that should appear in Android's compact
/// media notification view. When the notification is expanded, all [controls]
/// will be shown.
final List<int>? androidCompactActionIndices;
/// The set of system actions currently enabled. This is for specifying any
/// other [MediaAction]s that are not supported by [controls], because they do
/// not represent clickable buttons. For example:
///
/// * [MediaAction.seek] (enable a seek bar)
/// * [MediaAction.seekForward] (enable press-and-hold fast-forward control)
/// * [MediaAction.seekBackward] (enable press-and-hold rewind control)
///
/// Note that specifying [MediaAction.seek] in [systemActions] will enable a
/// seek bar in both the Android notification and the iOS control center, but
/// on Android, it will show only if the media item's duration has been set.
/// [MediaAction.seekForward] and [MediaAction.seekBackward] have a special
/// behaviour on iOS in which if you have already enabled the
/// [MediaAction.skipToNext] and [MediaAction.skipToPrevious] buttons, these
/// additional actions will allow the user to press and hold the buttons to
/// activate the continuous seeking behaviour.
///
/// When enabling the seek bar, also note that some Android devices will not
/// render the seek bar correctly unless your [AudioServiceConfig.androidNotificationIcon]
/// is a monochrome white icon on a transparent background, and your
/// [AudioServiceConfig.notificationColor] is a non-transparent color.
final Set<MediaAction> systemActions;
/// The playback position at [updateTime].
///
/// For efficiency, the [updatePosition] should NOT be updated continuously in
/// real time. Instead, it should be updated only when the normal continuity
/// of time is disrupted, such as during a seek, buffering and seeking. When
/// broadcasting such a position change, the [updateTime] specifies the time
/// of that change, allowing clients to project the realtime value of the
/// position as `position + (DateTime.now() - updateTime)`. As a convenience,
/// this calculation is provided by the [position] getter.
final Duration updatePosition;
/// The buffered position.
final Duration bufferedPosition;
/// The current playback speed where 1.0 means normal speed.
final double speed;
/// The time at which the playback position was last updated.
final DateTime updateTime;
/// The error code when [processingState] is [AudioProcessingState.error].
final int? errorCode;
/// The error message when [processingState] is [AudioProcessingState.error].
final String? errorMessage;
/// The current repeat mode.
final AudioServiceRepeatMode repeatMode;
/// The current shuffle mode.
final AudioServiceShuffleMode shuffleMode;
/// Whether captioning is enabled.
final bool captioningEnabled;
/// The index of the current item in the queue, if any.
final int? queueIndex;
/// Creates a [PlaybackState] with given field values, and with [updateTime]
/// defaulting to [DateTime.now].
PlaybackState({
this.processingState = AudioProcessingState.idle,
this.playing = false,
this.controls = const [],
this.androidCompactActionIndices,
this.systemActions = const {},
this.updatePosition = Duration.zero,
this.bufferedPosition = Duration.zero,
this.speed = 1.0,
DateTime? updateTime,
this.errorCode,
this.errorMessage,
this.repeatMode = AudioServiceRepeatMode.none,
this.shuffleMode = AudioServiceShuffleMode.none,
this.captioningEnabled = false,
this.queueIndex,
}) : assert(androidCompactActionIndices == null ||
androidCompactActionIndices.length <= 3),
updateTime = updateTime ?? clock.now();
/// Creates a copy of this state with given fields replaced by new values,
/// with [updateTime] set to [DateTime.now], and unless otherwise replaced,
/// with [updatePosition] set to [position].
///
/// The [errorCode] and [errorMessage] will be set to null unless [processingState] is
/// [AudioProcessingState.error].
PlaybackStateCopyWith get copyWith => _PlaybackStateCopyWith(this);
/// The current playback position.
Duration get position {
if (playing && processingState == AudioProcessingState.ready) {
return Duration(
milliseconds: (updatePosition.inMilliseconds +
speed *
(clock.now().millisecondsSinceEpoch -
updateTime.millisecondsSinceEpoch))
.toInt(),
);
} else {
return updatePosition;
}
}
PlaybackStateMessage _toMessage() => PlaybackStateMessage(
processingState:
AudioProcessingStateMessage.values[processingState.index],
playing: playing,
controls: controls.map((control) => control._toMessage()).toList(),
androidCompactActionIndices: androidCompactActionIndices,
systemActions: systemActions
.map((action) => MediaActionMessage.values[action.index])
.toSet(),
updatePosition: updatePosition,
bufferedPosition: bufferedPosition,
speed: speed,
updateTime: updateTime,
errorCode: errorCode,
errorMessage: errorMessage,
repeatMode: AudioServiceRepeatModeMessage.values[repeatMode.index],
shuffleMode: AudioServiceShuffleModeMessage.values[shuffleMode.index],
captioningEnabled: captioningEnabled,
queueIndex: queueIndex,
);
@override
String toString() => '${_toMessage().toMap()}';
@override
int get hashCode => Object.hash(
processingState,
playing,
Object.hashAll(controls),
androidCompactActionIndices != null
? Object.hashAll(androidCompactActionIndices!)
: 0,
Object.hashAll(systemActions),
updatePosition,
bufferedPosition,
speed,
updateTime,
errorCode,
errorMessage,
repeatMode,
shuffleMode,
captioningEnabled,
queueIndex,
);
@override
bool operator ==(Object other) =>
identical(other, this) ||
other.runtimeType == runtimeType &&
other is PlaybackState &&
processingState == other.processingState &&
playing == other.playing &&
listEquals(controls, other.controls) &&
listEquals(
androidCompactActionIndices, other.androidCompactActionIndices) &&
setEquals(systemActions, other.systemActions) &&
updatePosition == other.updatePosition &&
bufferedPosition == other.bufferedPosition &&
speed == other.speed &&
updateTime == other.updateTime &&
errorCode == other.errorCode &&
errorMessage == other.errorMessage &&
repeatMode == other.repeatMode &&
shuffleMode == other.shuffleMode &&
captioningEnabled == other.captioningEnabled &&
queueIndex == other.queueIndex;
}
/// The `copyWith` function type for [PlaybackState].
abstract class PlaybackStateCopyWith {
/// Calls this function.
PlaybackState call({
AudioProcessingState processingState,
bool playing,
List<MediaControl> controls,
List<int>? androidCompactActionIndices,
Set<MediaAction> systemActions,
Duration updatePosition,
Duration bufferedPosition,
double speed,
int? errorCode,
String? errorMessage,
AudioServiceRepeatMode repeatMode,
AudioServiceShuffleMode shuffleMode,
bool captioningEnabled,
int? queueIndex,
});
}
/// The implementation of [PlaybackState]'s `copyWith` function allowing
/// parameters to be explicitly set to null.
class _PlaybackStateCopyWith extends PlaybackStateCopyWith {
static const _fakeNull = Object();
/// The [PlaybackState] object this function applies to.
final PlaybackState value;
_PlaybackStateCopyWith(this.value);
@override
PlaybackState call({
Object? processingState = _fakeNull,
Object? playing = _fakeNull,
Object? controls = _fakeNull,
Object? androidCompactActionIndices = _fakeNull,
Object? systemActions = _fakeNull,
Object? updatePosition = _fakeNull,
Object? bufferedPosition = _fakeNull,
Object? speed = _fakeNull,
Object? errorCode = _fakeNull,
Object? errorMessage = _fakeNull,
Object? repeatMode = _fakeNull,
Object? shuffleMode = _fakeNull,
Object? captioningEnabled = _fakeNull,
Object? queueIndex = _fakeNull,
}) =>
PlaybackState(
processingState: processingState == _fakeNull
? value.processingState
: processingState as AudioProcessingState,
playing: playing == _fakeNull ? value.playing : playing as bool,
controls: controls == _fakeNull
? value.controls
: controls as List<MediaControl>,
androidCompactActionIndices: androidCompactActionIndices == _fakeNull
? value.androidCompactActionIndices
: androidCompactActionIndices as List<int>?,
systemActions: systemActions == _fakeNull
? value.systemActions
: systemActions as Set<MediaAction>,
updatePosition: updatePosition == _fakeNull
? value.updatePosition
: updatePosition as Duration,
bufferedPosition: bufferedPosition == _fakeNull
? value.bufferedPosition
: bufferedPosition as Duration,
speed: speed == _fakeNull ? value.speed : speed as double,
errorCode: errorCode == _fakeNull ? value.errorCode : errorCode as int?,
errorMessage: errorMessage == _fakeNull
? value.errorMessage
: errorMessage as String?,
repeatMode: repeatMode == _fakeNull
? value.repeatMode
: repeatMode as AudioServiceRepeatMode,
shuffleMode: shuffleMode == _fakeNull
? value.shuffleMode
: shuffleMode as AudioServiceShuffleMode,
captioningEnabled: captioningEnabled == _fakeNull
? value.captioningEnabled
: captioningEnabled as bool,
queueIndex:
queueIndex == _fakeNull ? value.queueIndex : queueIndex as int?,
);
}
/// The style of a [Rating].
enum RatingStyle {
/// Indicates a rating style is not supported.
///
/// A [Rating] will never have this type, but can be used by other classes
/// to indicate they do not support [Rating].
none,
/// A rating style with a single degree of rating, "heart" vs "no heart".
///
/// Can be used to indicate the content referred to is a favorite (or not).
heart,
/// A rating style for "thumb up" vs "thumb down".
thumbUpDown,
/// A rating style with 0 to 3 stars.
range3stars,
/// A rating style with 0 to 4 stars.
range4stars,
/// A rating style with 0 to 5 stars.
range5stars,
/// A rating style expressed as a percentage.
percentage,
}
/// A rating to attach to a MediaItem.
class Rating {
final RatingStyle _type;
final Object? _value;
const Rating._(this._type, this._value);
/// Creates a new heart rating.
const Rating.newHeartRating(bool hasHeart)
: this._(RatingStyle.heart, hasHeart);
/// Creates a new percentage rating.
const Rating.newPercentageRating(double percent)
: assert(
percent >= 0 && percent <= 100,
'Percentage must be in range from 0 to 100',
),
_type = RatingStyle.percentage,
_value = percent;
/// Creates a new star rating.
Rating.newStarRating(RatingStyle style, int rating)
: assert(
style == RatingStyle.range3stars ||
style == RatingStyle.range4stars ||
style == RatingStyle.range5stars,
'Invalid rating style',
),
assert(rating >= 0 && rating <= style.index),
_type = style,
_value = rating;
/// Creates a new thumb rating.
const Rating.newThumbRating(bool isThumbsUp)
: this._(RatingStyle.thumbUpDown, isThumbsUp);
/// Creates a new unrated rating.
const Rating.newUnratedRating(RatingStyle ratingStyle)
: this._(ratingStyle, null);
/// Return the rating style.
RatingStyle getRatingStyle() => _type;
/// Returns a percentage rating value greater or equal to `0.0`, or a
/// negative value if the rating style is not percentage-based, or
/// if it is unrated.
double getPercentRating() {
if (_type != RatingStyle.percentage) return -1;
final localValue = _value as double?;
if (localValue == null || localValue < 0 || localValue > 100) return -1;
return localValue;
}
/// Returns a rating value greater or equal to `0.0`, or a negative
/// value if the rating style is not star-based, or if it is
/// unrated.
int getStarRating() {
if (_type != RatingStyle.range3stars &&
_type != RatingStyle.range4stars &&
_type != RatingStyle.range5stars) {
return -1;
}
return _value as int? ?? -1;
}
/// Returns true if the rating is "heart selected" or false if the
/// rating is "heart unselected", if the rating style is not [RatingStyle.heart]
/// or if it is unrated.
bool hasHeart() {
if (_type != RatingStyle.heart) return false;
return _value as bool? ?? false;
}
/// Returns true if the rating is "thumb up" or false if the rating
/// is "thumb down", if the rating style is not [RatingStyle.thumbUpDown] or if
/// it is unrated.
bool isThumbUp() {
if (_type != RatingStyle.thumbUpDown) return false;
return _value as bool? ?? false;
}
/// Return whether there is a rating value available.
bool isRated() => _value != null;
RatingMessage _toMessage() => RatingMessage(
type: RatingStyleMessage.values[_type.index],
value: _value,
);
@override
String toString() => '${_toMessage().toMap()}';
@override
int get hashCode => Object.hash(_value, _type);
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is Rating &&
_type == other._type &&
_value == other._value;
}
/// Metadata of an audio item that can be played, or a folder containing
/// audio items.
class MediaItem {
/// A unique id.
final String id;
/// The title of this media item.
final String title;
/// The album this media item belongs to.
final String? album;
/// The artist of this media item.
final String? artist;
/// The genre of this media item.
final String? genre;
/// The duration of this media item.
final Duration? duration;
/// The artwork URI for this media item.
///
/// Supported types of URIs are:
///
/// * File - file://
/// * Network - http:// https:// etc.
/// * Android content URIs - content://
///
/// ## Speeding up Android content URI loading
///
/// For Android content:// URIs, the plugin by default uses
/// `ContentResolver.openFileDescriptor`, which takes the direct URI of an
/// image.
///
/// On Android API >= 29 there is `ContentResolver.loadThumbnail` function
/// which takes a URI of some content (for example, a song from `MediaStore`),
/// and returns a thumbnail for it.
///
/// It is noticeably faster to use this function. You can enable this by
/// putting a `loadThumbnailUri` key into the [extras]. If `loadThumbnail` is
/// not available, it will just fallback to using `openFileDescriptor`.
final Uri? artUri;
/// The HTTP headers to use when sending an HTTP request for [artUri].
final Map<String, String>? artHeaders;
/// Whether this is playable (i.e. not a folder).
final bool? playable;
/// Override the default title for display purposes.
final String? displayTitle;
/// Override the default subtitle for display purposes.
final String? displaySubtitle;
/// Override the default description for display purposes.
final String? displayDescription;
/// The rating of the media item.
final Rating? rating;
/// Whether this is a live stream.
final bool? isLive;
/// A map of additional metadata for the media item.
///
/// The values must be of type `int`, `String`, `bool` or `double`.
final Map<String, dynamic>? extras;
/// Creates a [MediaItem].
///
/// The [id] must be unique for each instance.
const MediaItem({
required this.id,
required this.title,
this.album,
this.artist,
this.genre,
this.duration,
this.artUri,
this.artHeaders,
this.playable = true,
this.displayTitle,
this.displaySubtitle,
this.displayDescription,
this.rating,
this.isLive,
this.extras,
});
/// Creates a copy of this [MediaItem] with with the given fields replaced by
/// new values.
MediaItemCopyWith get copyWith => _MediaItemCopyWith(this);
@override
int get hashCode => id.hashCode;
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType && other is MediaItem && other.id == id;
MediaItemMessage _toMessage() => MediaItemMessage(
id: id,
album: album,
title: title,
artist: artist,
genre: genre,
duration: duration,
artUri: artUri,
playable: playable,
displayTitle: displayTitle,
displaySubtitle: displaySubtitle,
displayDescription: displayDescription,
rating: rating?._toMessage(),
isLive: isLive,
extras: extras,
);
@override
String toString() => '${_toMessage().toMap()}';
}
/// The `copyWith` function type for [MediaItem].
abstract class MediaItemCopyWith {
/// Calls this function.
MediaItem call({
String id,
String title,
String? album,
String? artist,
String? genre,
Duration? duration,
Uri? artUri,
bool? playable,
String? displayTitle,
String? displaySubtitle,
String? displayDescription,
Rating? rating,
bool? isLive,
Map<String, dynamic>? extras,
});
}
/// The implementation of [MediaItem]'s `copyWith` function allowing
/// parameters to be explicitly set to null.
class _MediaItemCopyWith extends MediaItemCopyWith {
static const _fakeNull = Object();
/// The [MediaItem] object this function applies to.
final MediaItem value;
_MediaItemCopyWith(this.value);
@override
MediaItem call({
Object? id = _fakeNull,
Object? title = _fakeNull,
Object? album = _fakeNull,
Object? artist = _fakeNull,
Object? genre = _fakeNull,
Object? duration = _fakeNull,
Object? artUri = _fakeNull,
Object? playable = _fakeNull,
Object? displayTitle = _fakeNull,
Object? displaySubtitle = _fakeNull,
Object? displayDescription = _fakeNull,
Object? rating = _fakeNull,
Object? isLive = _fakeNull,
Object? extras = _fakeNull,
}) =>
MediaItem(
id: id == _fakeNull ? value.id : id as String,
title: title == _fakeNull ? value.title : title as String,
album: album == _fakeNull ? value.album : album as String?,
artist: artist == _fakeNull ? value.artist : artist as String?,
genre: genre == _fakeNull ? value.genre : genre as String?,
duration:
duration == _fakeNull ? value.duration : duration as Duration?,
artUri: artUri == _fakeNull ? value.artUri : artUri as Uri?,
playable: playable == _fakeNull ? value.playable : playable as bool?,
displayTitle: displayTitle == _fakeNull
? value.displayTitle
: displayTitle as String?,
displaySubtitle: displaySubtitle == _fakeNull
? value.displaySubtitle
: displaySubtitle as String?,
displayDescription: displayDescription == _fakeNull
? value.displayDescription
: displayDescription as String?,
rating: rating == _fakeNull ? value.rating : rating as Rating?,
isLive: isLive == _fakeNull ? value.isLive : isLive as bool?,
extras: extras == _fakeNull
? value.extras
: extras as Map<String, dynamic>?,
);
}
/// Custom action information used to define an action name and optional extras
/// that are sent to [AudioHandler.customAction] when the associated media control is used.
class CustomMediaAction {
/// Custom action name
final String name;
/// A map of additional data for the custom action.
///
/// The values must be integers or strings.
final Map<String, dynamic>? extras;
/// Creates a [CustomMediaAction].
const CustomMediaAction({required this.name, this.extras});
/// Convert to a Map.
Map<String, dynamic> toMap() => <String, dynamic>{
'name': name,
'extras': extras,
};
CustomMediaActionMessage _toMessage() => CustomMediaActionMessage(
name: name,
extras: extras,
);
@override
int get hashCode => Object.hash(name, extras);
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is CustomMediaAction &&
name == other.name &&
mapEquals<String, dynamic>(extras, other.extras);
}
/// A button to appear in the Android notification, lock screen, Android smart
/// watch, or Android Auto device. The set of buttons you would like to display
/// at any given moment should be streamed via [AudioHandler.playbackState].
///
/// Each [MediaControl] button controls a specified [MediaAction]. Only the
/// following actions can be represented as buttons:
///
/// * [MediaAction.stop]
/// * [MediaAction.pause]
/// * [MediaAction.play]
/// * [MediaAction.rewind]
/// * [MediaAction.skipToPrevious]
/// * [MediaAction.skipToNext]
/// * [MediaAction.fastForward]
/// * [MediaAction.playPause]
///
/// Predefined controls with default Android icons and labels are defined as
/// static fields of this class. If you wish to define your own custom Android
/// controls with your own icon resources, you will need to place the Android
/// resources in `android/app/src/main/res`. Here, you will find a subdirectory
/// for each different resolution:
///
/// ```
/// drawable-hdpi
/// drawable-mdpi
/// drawable-xhdpi
/// drawable-xxhdpi
/// drawable-xxxhdpi
/// ```
///
/// You can use [Android Asset
/// Studio](https://romannurik.github.io/AndroidAssetStudio/) to generate these
/// different subdirectories for any standard material design icon.
class MediaControl {
/// A default control for [MediaAction.stop].
static const stop = MediaControl(
androidIcon: 'drawable/audio_service_stop',
label: 'Stop',
action: MediaAction.stop,
);
/// A default control for [MediaAction.pause].
static const pause = MediaControl(
androidIcon: 'drawable/audio_service_pause',
label: 'Pause',
action: MediaAction.pause,
);
/// A default control for [MediaAction.play].
static const play = MediaControl(
androidIcon: 'drawable/audio_service_play_arrow',
label: 'Play',
action: MediaAction.play,
);
/// A default control for [MediaAction.rewind].
static const rewind = MediaControl(
androidIcon: 'drawable/audio_service_fast_rewind',
label: 'Rewind',
action: MediaAction.rewind,
);
/// A default control for [MediaAction.skipToNext].
static const skipToNext = MediaControl(
androidIcon: 'drawable/audio_service_skip_next',
label: 'Next',
action: MediaAction.skipToNext,
);
/// A default control for [MediaAction.skipToPrevious].
static const skipToPrevious = MediaControl(
androidIcon: 'drawable/audio_service_skip_previous',
label: 'Previous',
action: MediaAction.skipToPrevious,
);
/// A default control for [MediaAction.fastForward].
static const fastForward = MediaControl(
androidIcon: 'drawable/audio_service_fast_forward',
label: 'Fast Forward',
action: MediaAction.fastForward,
);
/// A reference to an Android icon resource for the control (e.g.
/// `"drawable/ic_action_pause"`)
final String androidIcon;
/// A label for the control
final String label;
/// The action to be executed by this control
final MediaAction action;
/// The custom action name and optional extras to receive in
/// [AudioHandler.customAction]
final CustomMediaAction? customAction;
/// Creates a custom [MediaControl].
MediaControl.custom({
required this.androidIcon,
required this.label,
required String name,
Map<String, dynamic>? extras,
}) : action = MediaAction.custom,
customAction = CustomMediaAction(name: name, extras: extras) {
assert(action != MediaAction.custom || customAction != null);
}
/// Creates a custom [MediaControl].
const MediaControl({
required this.androidIcon,
required this.label,
required this.action,
this.customAction,
});
/// Creates a copy of this control with given fields replaced by new values.
MediaControl copyWith({
String? androidIcon,
String? label,
MediaAction? action,
CustomMediaAction? customAction,
}) =>
MediaControl(
androidIcon: androidIcon ?? this.androidIcon,
label: label ?? this.label,
action: action ?? this.action,
customAction: customAction ?? this.customAction,
);
MediaControlMessage _toMessage() => MediaControlMessage(
androidIcon: androidIcon,
label: label,
action: MediaActionMessage.values[action.index],
customAction: customAction?._toMessage(),
);
@override
String toString() => '${_toMessage().toMap()}';
@override
int get hashCode => Object.hash(androidIcon, label, action);
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is MediaControl &&
androidIcon == other.androidIcon &&
label == other.label &&
action == other.action;
}
/// Provides an API to manage the app's [AudioHandler]. An app must call [init]
/// during initialisation to register the [AudioHandler] that will service all
/// requests to play audio.
class AudioService {
/// The cache to use when loading artwork.
/// Defaults to [DefaultCacheManager].
static BaseCacheManager get cacheManager => _cacheManager!;
static BaseCacheManager? _cacheManager;
static late AudioServiceConfig _config;
static late AudioHandler _handler;
/// The current configuration.
static AudioServiceConfig get config => _config;
/// The root media ID for browsing media provided by the background
/// task.
static const String browsableRootId = 'root';
/// The root media ID for browsing the most recently played item(s).
static const String recentRootId = 'recent';
static final BehaviorSubject<bool> _notificationClicked =
BehaviorSubject.seeded(false);
/// A stream that broadcasts the status of the notificationClick event.
static ValueStream<bool> get notificationClicked => _notificationClicked;
static final _asyncError = PublishSubject<Object>();
/// A stream that broadcasts any exceptions that occur asynchronously.
static Stream<Object> get asyncError => _asyncError;
static final _compatibilitySwitcher = SwitchAudioHandler();
/// Register the app's [AudioHandler] with configuration options. This must be
/// called once during the app's initialisation so that it is prepared to
/// handle audio requests immediately after a cold restart (e.g. if the user
/// clicks on the play button in the media notification while your app is not
/// running and your app needs to be woken up).
///
/// You may optionally specify a [cacheManager] to use when loading artwork to
/// display in the media notification and lock screen. This defaults to
/// [DefaultCacheManager].
///
/// This may throw a [PlatformException] on Android if you have not set the
/// correct Service or Activity in your `AndroidManifest.xml` file or if your
/// Activity does not provide the correct `FlutterEngine`.
static Future<T> init<T extends AudioHandler>({
required T Function() builder,
AudioServiceConfig? config,
BaseCacheManager? cacheManager,
}) async {
assert(_cacheManager == null);
config ??= const AudioServiceConfig();
assert(config.fastForwardInterval > Duration.zero);
assert(config.rewindInterval > Duration.zero);
WidgetsFlutterBinding.ensureInitialized();
_cacheManager = (cacheManager ??= DefaultCacheManager());
final callbacks = _HandlerCallbacks();
_platform.setHandlerCallbacks(callbacks);
await _platform.configure(ConfigureRequest(config: config._toMessage()));
_config = config;
final handler = builder();
_handler = handler;
callbacks.setHandler(handler);
_observeMediaItem();
_observeAndroidPlaybackInfo();
_observeQueue();
_observePlaybackState();
return handler;
}
static Future<void> _observeMediaItem() async {
Object? artFetchOperationId;
_handler.mediaItem.listen((mediaItem) async {
if (mediaItem == null) {
return;
}
final operationId = Object();
artFetchOperationId = operationId;
final artUri = mediaItem.artUri;
if (artUri == null || artUri.scheme == 'content') {
_platform
.setMediaItem(
SetMediaItemRequest(mediaItem: mediaItem._toMessage()))
.catchError(_asyncError.add);
} else {
/// Sends media item to the platform.
/// We potentially need to fetch the art before that.
Future<void> sendToPlatform(String? filePath) async {
final extras = mediaItem.extras;
final platformMediaItem = mediaItem.copyWith(
extras: <String, dynamic>{
if (extras != null) ...extras,
'artCacheFile': filePath,
},
);
await _platform.setMediaItem(
SetMediaItemRequest(mediaItem: platformMediaItem._toMessage()));
}
if (artUri.scheme == 'file') {
sendToPlatform(artUri.toFilePath()).catchError(_asyncError.add);
} else {
// Try to load a cached file from memory.
final fileInfo =
await cacheManager.getFileFromMemory(artUri.toString());
final filePath = fileInfo?.file.path;
if (operationId != artFetchOperationId) {
return;
}
if (filePath != null) {
// If we successfully downloaded the art call to platform.
sendToPlatform(filePath).catchError(_asyncError.add);
} else {
// We haven't fetched the art yet, so show the metadata now, and again
// after we load the art.
try {
await _platform.setMediaItem(
SetMediaItemRequest(mediaItem: mediaItem._toMessage()));
} catch (e) {
_asyncError.add(e);
return;
}
if (operationId != artFetchOperationId) {
return;
}
// Load the art.
final loadedFilePath = await _loadArtwork(mediaItem);
if (operationId != artFetchOperationId) {
return;
}
// If we successfully downloaded the art, call to platform.
if (loadedFilePath != null) {
sendToPlatform(loadedFilePath).catchError(_asyncError.add);
}
}
}
}
});
}
static Future<void> _observeAndroidPlaybackInfo() async {
await for (var playbackInfo in _handler.androidPlaybackInfo) {
try {
await _platform.setAndroidPlaybackInfo(SetAndroidPlaybackInfoRequest(
playbackInfo: playbackInfo._toMessage(),
));
} catch (e) {
_asyncError.add(e);
}
}
}
static Future<void> _observeQueue() async {
await for (var queue in _handler.queue) {
if (_config.preloadArtwork) {
_loadAllArtwork(queue);
}
try {
await _platform.setQueue(SetQueueRequest(
queue: queue.map((item) => item._toMessage()).toList()));
} catch (e) {
_asyncError.add(e);
}
}
}
static Future<void> _observePlaybackState() async {
var previousState = _handler.playbackState.nvalue;
await for (var playbackState in _handler.playbackState) {
try {
await _platform
.setState(SetStateRequest(state: playbackState._toMessage()));
if (playbackState.processingState == AudioProcessingState.idle &&
previousState?.processingState != AudioProcessingState.idle) {
await AudioService._stop();
}
previousState = playbackState;
} catch (e) {
_asyncError.add(e);
}
}
}
/// A stream tracking the current position, suitable for animating a seek bar.
/// To ensure a smooth animation, this stream emits values more frequently on
/// short media items where the seek bar moves more quickly, and less
/// frequenly on long media items where the seek bar moves more slowly. The
/// interval between each update will be no quicker than once every 16ms and
/// no slower than once every 200ms.
///
/// See [createPositionStream] for more control over the stream parameters.
static final Stream<Duration> position = createPositionStream(
steps: 800,
minPeriod: const Duration(milliseconds: 16),
maxPeriod: const Duration(milliseconds: 200));
/// Creates a new stream periodically tracking the current position. The
/// stream will aim to emit [steps] position updates at intervals of
/// current [MediaItem.duration] / [steps]. This interval will be clipped between [minPeriod]
/// and [maxPeriod]. This stream will not emit values while audio playback is
/// paused or stalled.
///
/// Note: each time this method is called, a new stream is created. If you
/// intend to use this stream multiple times, you should hold a reference to
/// the returned stream.
static Stream<Duration> createPositionStream({
int steps = 800,
Duration minPeriod = const Duration(milliseconds: 200),
Duration maxPeriod = const Duration(milliseconds: 200),
}) {
assert(minPeriod <= maxPeriod);
assert(minPeriod > Duration.zero);
Duration? last;
late StreamController<Duration> controller;
late StreamSubscription<MediaItem?> mediaItemSubscription;
late StreamSubscription<PlaybackState> playbackStateSubscription;
Timer? currentTimer;
Duration duration() => _handler.mediaItem.nvalue?.duration ?? Duration.zero;
Duration step() {
var s = duration() ~/ steps;
if (s < minPeriod) s = minPeriod;
if (s > maxPeriod) s = maxPeriod;
return s;
}
void yieldPosition(Timer? timer) {
if (last != _handler.playbackState.nvalue?.position) {
controller.add((last = _handler.playbackState.nvalue?.position)!);
}
}
controller = StreamController.broadcast(
sync: true,
onListen: () {
mediaItemSubscription =
_handler.mediaItem.listen((MediaItem? mediaItem) {
// Potentially a new duration
currentTimer?.cancel();
currentTimer = Timer.periodic(step(), yieldPosition);
});
playbackStateSubscription =
_handler.playbackState.listen((PlaybackState state) {
// Potentially a time discontinuity
yieldPosition(currentTimer);
});
},
onCancel: () {
mediaItemSubscription.cancel();
playbackStateSubscription.cancel();
},
);
return controller.stream;
}
/// In Android, forces media button events to be routed to your active media
/// session.
///
/// This is necessary if you want to play TextToSpeech in the background and
/// still respond to media button events. You should call it just before
/// playing TextToSpeech.
///
/// This is not necessary if you are playing normal audio in the background
/// such as music because this kind of "normal" audio playback will
/// automatically qualify your app to receive media button events.
static Future<void> androidForceEnableMediaButtons() async {
await _platform.androidForceEnableMediaButtons(
const AndroidForceEnableMediaButtonsRequest(),
);
}
/// Stops the service.
static Future<void> _stop() async {
await _platform.stopService(const StopServiceRequest());
}
static Future<void> _loadAllArtwork(List<MediaItem> queue) async {
for (var mediaItem in queue) {
await _loadArtwork(mediaItem);
}
}
static Future<String?> _loadArtwork(MediaItem mediaItem) async {
try {
final artUri = mediaItem.artUri;
if (artUri != null) {
if (artUri.scheme == 'file') {
return artUri.toFilePath();
} else {
final headers = mediaItem.artHeaders;
final file = headers != null
? await cacheManager.getSingleFile(mediaItem.artUri!.toString(),
headers: headers)
: await cacheManager.getSingleFile(mediaItem.artUri!.toString());
return file.path;
}
}
} catch (e, st) {
// TODO: handle this somehow?
// ignore: avoid_print
print('Error loading artUri: $e\n$st');
}
return null;
}
// DEPRECATED members
/// Deprecated. Use [browsableRootId] instead.
@Deprecated("Use browsableRootId instead.")
// ignore: constant_identifier_names
static const String MEDIA_ROOT_ID = browsableRootId;
static final _browseMediaChildrenSubject = BehaviorSubject<List<MediaItem>>();
/// Deprecated. Directly subscribe to a parent's children via
/// [AudioHandler.subscribeToChildren].
@Deprecated("Use AudioHandler.subscribeToChildren instead.")
static Stream<List<MediaItem>> get browseMediaChildrenStream =>
_browseMediaChildrenSubject.stream;
/// Deprecated. Use [AudioHandler.getChildren] instead.
@Deprecated("Use AudioHandler.getChildren instead")
static List<MediaItem>? get browseMediaChildren =>
_browseMediaChildrenSubject.nvalue;
/// Deprecated. Use [AudioHandler.playbackState] instead.
@Deprecated("Use AudioHandler.playbackState instead.")
static ValueStream<PlaybackState> get playbackStateStream =>
_compatibilitySwitcher.playbackState;
/// Deprecated. Use [notificationClicked] instead.
@Deprecated("Use notificationClicked instead.")
static ValueStream<bool> get notificationClickEventStream =>
notificationClicked;
/// Deprecated. Use `value` of [AudioHandler.playbackState] instead.
@Deprecated("Use AudioHandler.playbackState.value instead.")
static PlaybackState get playbackState =>
_compatibilitySwitcher.playbackState.nvalue ?? PlaybackState();
/// Deprecated. Use [AudioHandler.mediaItem] instead.
@Deprecated("Use AudioHandler.mediaItem instead.")
static ValueStream<MediaItem?> get currentMediaItemStream =>
_compatibilitySwitcher.mediaItem;
/// Deprecated. Use `value` of [AudioHandler.mediaItem] instead.
@Deprecated("Use AudioHandler.mediaItem.value instead.")
static MediaItem? get currentMediaItem =>
_compatibilitySwitcher.mediaItem.nvalue;
/// Deprecated. Use [AudioHandler.queue] instead.
@Deprecated("Use AudioHandler.queue instead.")
static ValueStream<List<MediaItem>?> get queueStream =>
_compatibilitySwitcher.queue;
/// Deprecated. Use `value` of [AudioHandler.queue] instead.
@Deprecated("Use AudioHandler.queue.value instead.")
static List<MediaItem>? get queue => _compatibilitySwitcher.queue.nvalue;
/// Deprecated. Use [AudioHandler.customEvent] instead.
@Deprecated("Use AudioHandler.customEvent instead.")
static Stream<dynamic> get customEventStream =>
_compatibilitySwitcher.customEvent;
/// Deprecated. Use [AudioHandler.playbackState] instead.
@Deprecated("Use AudioHandler.playbackState instead.")
static ValueStream<bool> get runningStream => playbackStateStream
.map((state) => state.processingState != AudioProcessingState.idle)
as ValueStream<bool>;
/// Deprecated. Use [PlaybackState.processingState] of [AudioHandler.playbackState] instead.
@Deprecated("Use PlaybackState.processingState instead.")
static bool get running => runningStream.nvalue ?? false;
static StreamSubscription<Map<String, dynamic>>? _childrenSubscription;
/// Deprecated. The new [AudioHandler] API now automatically starts the
/// service when your implementation enters the playing state. Parameters can
/// be passed via [AudioHandler.customAction].
@Deprecated("Use init instead.")
static Future<bool> start({
required Function backgroundTaskEntrypoint,
Map<String, dynamic>? params,
String androidNotificationChannelName = "Notifications",
String? androidNotificationChannelDescription,
int? androidNotificationColor,
String androidNotificationIcon = 'mipmap/ic_launcher',
bool androidShowNotificationBadge = false,
bool androidNotificationClickStartsActivity = true,
bool androidNotificationOngoing = false,
bool androidResumeOnClick = true,
bool androidStopForegroundOnPause = false,
bool androidEnableQueue = false,
Size? androidArtDownscaleSize,
Duration fastForwardInterval = const Duration(seconds: 10),
Duration rewindInterval = const Duration(seconds: 10),
}) async {
if (!androidEnableQueue) {
// ignore: avoid_print
print('NOTE: androidEnableQueue is always true from 0.18.0 onwards.');
}
if (_cacheManager != null && _handler.playbackState.hasValue) {
if (_handler.playbackState.nvalue!.processingState !=
AudioProcessingState.idle) {
return false;
}
}
AudioServiceBackground._startCompleter = Completer<BackgroundAudioTask>();
backgroundTaskEntrypoint();
final task = await AudioServiceBackground._startCompleter!.future;
task._handler = _BackgroundAudioHandler();
task._handler._task = task;
AudioServiceBackground._startCompleter = null;
if (_cacheManager == null) {
_compatibilitySwitcher.inner = task._handler;
await init(
builder: () => _compatibilitySwitcher,
config: AudioServiceConfig(
androidResumeOnClick: androidResumeOnClick,
androidNotificationChannelName: androidNotificationChannelName,
androidNotificationChannelDescription:
androidNotificationChannelDescription,
notificationColor: androidNotificationColor != null
? Color(androidNotificationColor)
: null,
androidNotificationIcon: androidNotificationIcon,
androidShowNotificationBadge: androidShowNotificationBadge,
androidNotificationClickStartsActivity:
androidNotificationClickStartsActivity,
androidNotificationOngoing: androidNotificationOngoing,
androidStopForegroundOnPause: androidStopForegroundOnPause,
artDownscaleWidth: androidArtDownscaleSize?.width.round(),
artDownscaleHeight: androidArtDownscaleSize?.height.round(),
fastForwardInterval: fastForwardInterval,
rewindInterval: rewindInterval,
),
);
} else {
_compatibilitySwitcher.inner = task._handler;
}
await task.onStart(params);
return true;
}
/// Deprecated. Instead, subscribe directly to a parent's children via
/// [AudioHandler.subscribeToChildren].
@Deprecated("Use AudioHandler.subscribeToChildren instead.")
static Future<void> setBrowseMediaParent(
[String parentMediaId = browsableRootId]) async {
_childrenSubscription?.cancel();
_childrenSubscription = _compatibilitySwitcher
.subscribeToChildren(parentMediaId)
.listen((Map<String, dynamic>? options) async {
_browseMediaChildrenSubject
.add(await _compatibilitySwitcher.getChildren(parentMediaId));
});
}
/// Deprecated. Use [AudioHandler.addQueueItem] instead.
@Deprecated("Use AudioHandler.addQueueItem instead.")
static final addQueueItem = _compatibilitySwitcher.addQueueItem;
/// Deprecated. Use [AudioHandler.insertQueueItem] instead.
@Deprecated("Use AudioHandler.insertQueueItem instead.")
static Future<void> addQueueItemAt(MediaItem mediaItem, int index) async {
await _compatibilitySwitcher.insertQueueItem(index, mediaItem);
}
/// Deprecated. Use [AudioHandler.removeQueueItem] instead.
@Deprecated("Use AudioHandler.removeQueueItem instead.")
static final removeQueueItem = _compatibilitySwitcher.removeQueueItem;
/// Deprecated. Use [AudioHandler.addQueueItems] instead.
@Deprecated("Use AudioHandler.addQueueItems instead.")
static Future<void> addQueueItems(List<MediaItem> mediaItems) async {
for (var mediaItem in mediaItems) {
await addQueueItem(mediaItem);
}
}
/// Deprecated. Use [AudioHandler.updateQueue] instead.
@Deprecated("Use AudioHandler.updateQueue instead.")
static final updateQueue = _compatibilitySwitcher.updateQueue;
/// Deprecated. Use [AudioHandler.updateMediaItem] instead.
@Deprecated("Use AudioHandler.updateMediaItem instead.")
static final updateMediaItem = _compatibilitySwitcher.updateMediaItem;
/// Deprecated. Use [AudioHandler.click] instead.
@Deprecated("Use AudioHandler.click instead.")
static final Future<void> Function([MediaButton]) click =
_compatibilitySwitcher.click;
/// Deprecated. Use [AudioHandler.prepare] instead.
@Deprecated("Use AudioHandler.prepare instead.")
static final prepare = _compatibilitySwitcher.prepare;
/// Deprecated. Use [AudioHandler.prepareFromMediaId] instead.
@Deprecated("Use AudioHandler.prepareFromMediaId instead.")
static final Future<void> Function(String, [Map<String, dynamic>])
prepareFromMediaId = _compatibilitySwitcher.prepareFromMediaId;
/// Deprecated. Use [AudioHandler.play] instead.
@Deprecated("Use AudioHandler.play instead.")
static final play = _compatibilitySwitcher.play;
/// Deprecated. Use [AudioHandler.playFromMediaId] instead.
@Deprecated("Use AudioHandler.playFromMediaId instead.")
static final Future<void> Function(String, [Map<String, dynamic>])
playFromMediaId = _compatibilitySwitcher.playFromMediaId;
/// Deprecated. Use [AudioHandler.playMediaItem] instead.
@Deprecated("Use AudioHandler.playMediaItem instead.")
static final playMediaItem = _compatibilitySwitcher.playMediaItem;
/// Deprecated. Use [AudioHandler.skipToQueueItem] instead.
@Deprecated("Use AudioHandler.skipToQueueItem instead.")
static Future<void> skipToQueueItem(String mediaId) async {
final queue = _compatibilitySwitcher.queue.nvalue!;
final index = queue.indexWhere((item) => item.id == mediaId);
await _compatibilitySwitcher.skipToQueueItem(index);
}
/// Deprecated. Use [AudioHandler.pause] instead.
@Deprecated("Use AudioHandler.pause instead.")
static final pause = _compatibilitySwitcher.pause;
/// Deprecated. Use [AudioHandler.stop] instead.
@Deprecated("Use AudioHandler.stop instead.")
static final stop = _compatibilitySwitcher.stop;
/// Deprecated. Use [AudioHandler.seek] instead.
@Deprecated("Use AudioHandler.seek instead.")
static final seekTo = _compatibilitySwitcher.seek;
/// Deprecated. Use [AudioHandler.skipToNext] instead.
@Deprecated("Use AudioHandler.skipToNext instead.")
static final skipToNext = _compatibilitySwitcher.skipToNext;
/// Deprecated. Use [AudioHandler.skipToPrevious] instead.
@Deprecated("Use AudioHandler.skipToPrevious instead.")
static final skipToPrevious = _compatibilitySwitcher.skipToPrevious;
/// Deprecated. Use [AudioHandler.fastForward] instead.
@Deprecated("Use AudioHandler.fastForward instead.")
static final Future<void> Function() fastForward =
_compatibilitySwitcher.fastForward;
/// Deprecated. Use [AudioHandler.rewind] instead.
@Deprecated("Use AudioHandler.rewind instead.")
static final Future<void> Function() rewind = _compatibilitySwitcher.rewind;
/// Deprecated. Use [AudioHandler.setRepeatMode] instead.
@Deprecated("Use AudioHandler.setRepeatMode instead.")
static final setRepeatMode = _compatibilitySwitcher.setRepeatMode;
/// Deprecated. Use [AudioHandler.setShuffleMode] instead.
@Deprecated("Use AudioHandler.setShuffleMode instead.")
static final setShuffleMode = _compatibilitySwitcher.setShuffleMode;
/// Deprecated. Use [AudioHandler.setRating] instead.
@Deprecated("Use AudioHandler.setRating instead.")
static Future<void> setRating(Rating rating, Map<dynamic, dynamic> extras) =>
_compatibilitySwitcher.setRating(rating, extras.cast<String, dynamic>());
/// Deprecated. Use [AudioHandler.setSpeed] instead.
@Deprecated("Use AudioHandler.setSpeed instead.")
static final setSpeed = _compatibilitySwitcher.setSpeed;
/// Deprecated. Use [AudioHandler.seekBackward] instead.
@Deprecated("Use audioHandler.seekBackward instead.")
static final seekBackward = _compatibilitySwitcher.seekBackward;
/// Deprecated. Use [AudioHandler.seekForward] instead.
@Deprecated("Use AudioHandler.seekForward instead.")
static final seekForward = _compatibilitySwitcher.seekForward;
/// Deprecated. Use [AudioHandler.customAction] instead.
@Deprecated("Use AudioHandler.customAction instead.")
static final Future<dynamic> Function(String, Map<String, dynamic>)
customAction = _compatibilitySwitcher.customAction;
/// Deprecated. Use [position] instead.
@Deprecated("Use position instead.")
static final ValueStream<Duration> positionStream =
BehaviorSubject.seeded(Duration.zero, sync: true)
..addStream(position)
..stream;
}
class _BackgroundAudioHandler extends BaseAudioHandler {
// ignore: deprecated_member_use_from_same_package
late BackgroundAudioTask _task;
@override
// ignore: deprecated_member_use_from_same_package
Future<void> prepare() => _task.onPrepare();
@override
Future<void> prepareFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) =>
// ignore: deprecated_member_use_from_same_package
_task.onPrepareFromMediaId(mediaId);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> play() => _task.onPlay();
@override
Future<void> playFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) =>
// ignore: deprecated_member_use_from_same_package
_task.onPlayFromMediaId(mediaId);
@override
Future<void> playMediaItem(MediaItem mediaItem) =>
// ignore: deprecated_member_use_from_same_package
_task.onPlayMediaItem(mediaItem);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> pause() => _task.onPause();
@override
Future<void> click([MediaButton button = MediaButton.media]) =>
// ignore: deprecated_member_use_from_same_package
_task.onClick(button);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> stop() => _task.onStop();
@override
Future<void> addQueueItem(MediaItem mediaItem) =>
// ignore: deprecated_member_use_from_same_package
_task.onAddQueueItem(mediaItem);
@override
Future<void> addQueueItems(List<MediaItem> mediaItems) async {
for (var mediaItem in mediaItems) {
// ignore: deprecated_member_use_from_same_package
await _task.onAddQueueItem(mediaItem);
}
}
@override
Future<void> insertQueueItem(int index, MediaItem mediaItem) =>
// ignore: deprecated_member_use_from_same_package
_task.onAddQueueItemAt(mediaItem, index);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> updateQueue(List<MediaItem> queue) => _task.onUpdateQueue(queue);
@override
Future<void> updateMediaItem(MediaItem mediaItem) =>
// ignore: deprecated_member_use_from_same_package
_task.onUpdateMediaItem(mediaItem);
@override
Future<void> removeQueueItem(MediaItem mediaItem) =>
// ignore: deprecated_member_use_from_same_package
_task.onRemoveQueueItem(mediaItem);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> skipToNext() => _task.onSkipToNext();
@override
// ignore: deprecated_member_use_from_same_package
Future<void> skipToPrevious() => _task.onSkipToPrevious();
@override
// ignore: deprecated_member_use_from_same_package
Future<void> fastForward() => _task.onFastForward();
@override
// ignore: deprecated_member_use_from_same_package
Future<void> rewind() => _task.onRewind();
@override
Future<void> skipToQueueItem(int index) async {
final queue = this.queue.nvalue ?? <MediaItem>[];
if (index < 0 || index >= queue.length) return;
final mediaItem = queue[index];
// ignore: deprecated_member_use_from_same_package
await _task.onSkipToQueueItem(mediaItem.id);
}
@override
// ignore: deprecated_member_use_from_same_package
Future<void> seek(Duration position) => _task.onSeekTo(position);
@override
Future<void> setRating(Rating rating, [Map<String, dynamic>? extras]) =>
// ignore: deprecated_member_use_from_same_package
_task.onSetRating(rating, extras);
@override
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) =>
// ignore: deprecated_member_use_from_same_package
_task.onSetRepeatMode(repeatMode);
@override
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) =>
// ignore: deprecated_member_use_from_same_package
_task.onSetShuffleMode(shuffleMode);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> seekBackward(bool begin) => _task.onSeekBackward(begin);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> seekForward(bool begin) => _task.onSeekForward(begin);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> setSpeed(double speed) => _task.onSetSpeed(speed);
@override
Future<dynamic> customAction(String name, [Map<String, dynamic>? extras]) =>
// ignore: deprecated_member_use_from_same_package
_task.onCustomAction(name, extras);
@override
// ignore: deprecated_member_use_from_same_package
Future<void> onNotificationDeleted() => _task.onClose();
@override
Future<List<MediaItem>> getChildren(String parentMediaId,
[Map<String, dynamic>? options]) =>
// ignore: deprecated_member_use_from_same_package
_task.onLoadChildren(parentMediaId);
}
/// This class is deprecated. Use [BaseAudioHandler] instead.
@Deprecated("Use AudioHandler instead.")
abstract class BackgroundAudioTask {
late _BackgroundAudioHandler _handler;
/// Deprecated. Use [AudioServiceConfig.fastForwardInterval] from [AudioService.config] instead.
@Deprecated(
"Use [AudioServiceConfig.fastForwardInterval] from [AudioService.config] instead.")
Duration get fastForwardInterval => AudioService.config.fastForwardInterval;
/// Deprecated. Use [AudioServiceConfig.rewindInterval] from [AudioService.config] instead.
@Deprecated(
"Use [AudioServiceConfig.rewindInterval] from [AudioService.config] instead.")
Duration get rewindInterval => AudioService.config.rewindInterval;
/// Deprecated. The new [AudioHandler] API now automatically starts the
/// service when your implementation enters the playing state. Parameters can
/// be passed via [AudioHandler.customAction].
@Deprecated("Use AudioService.init instead.")
Future<void> onStart(Map<String, dynamic>? params) async {}
/// Deprecated. Replaced by [AudioHandler.stop].
@mustCallSuper
@Deprecated("Use AudioHandler.stop instead.")
Future<void> onStop() async {
final audioSession = await AudioSession.instance;
try {
await audioSession.setActive(false);
} catch (e) {
// ignore: avoid_print
print("While deactivating audio session: $e");
}
}
/// Deprecated. Replaced by [AudioHandler.getChildren].
@Deprecated("Use AudioHandler.getChildren instead.")
Future<List<MediaItem>> onLoadChildren(String parentMediaId) async => [];
/// Deprecated. Replaced by [AudioHandler.click].
@Deprecated("Use AudioHandler.click instead.")
Future<void> onClick(MediaButton? button) async {
switch (button!) {
case MediaButton.media:
if (_handler.playbackState.nvalue!.playing) {
await onPause();
} else {
await onPlay();
}
break;
case MediaButton.next:
await onSkipToNext();
break;
case MediaButton.previous:
await onSkipToPrevious();
break;
}
}
/// Deprecated. Replaced by [AudioHandler.pause].
@Deprecated("Use AudioHandler.pause instead.")
Future<void> onPause() async {}
/// Deprecated. Replaced by [AudioHandler.prepare].
@Deprecated("Use AudioHandler.prepare instead.")
Future<void> onPrepare() async {}
/// Deprecated. Replaced by [AudioHandler.prepareFromMediaId].
@Deprecated("Use AudioHandler.prepareFromMediaId instead.")
Future<void> onPrepareFromMediaId(String mediaId) async {}
/// Deprecated. Replaced by [AudioHandler.play].
@Deprecated("Use AudioHandler.play instead.")
Future<void> onPlay() async {}
/// Deprecated. Replaced by [AudioHandler.playFromMediaId].
@Deprecated("Use AudioHandler.playFromMediaId instead.")
Future<void> onPlayFromMediaId(String mediaId) async {}
/// Deprecated. Replaced by [AudioHandler.playMediaItem].
@Deprecated("Use AudioHandler.playMediaItem instead.")
Future<void> onPlayMediaItem(MediaItem mediaItem) async {}
/// Deprecated. Replaced by [AudioHandler.addQueueItem].
@Deprecated("Use AudioHandler.addQueueItem instead.")
Future<void> onAddQueueItem(MediaItem mediaItem) async {}
/// Deprecated. Replaced by [AudioHandler.updateQueue].
@Deprecated("Use AudioHandler.updateQueue instead.")
Future<void> onUpdateQueue(List<MediaItem> queue) async {}
/// Deprecated. Replaced by [AudioHandler.updateMediaItem].
@Deprecated("Use AudioHandler.updateMediaItem instead.")
Future<void> onUpdateMediaItem(MediaItem mediaItem) async {}
/// Deprecated. Replaced by [AudioHandler.insertQueueItem].
@Deprecated("Use AudioHandler.insertQueueItem instead.")
Future<void> onAddQueueItemAt(MediaItem mediaItem, int index) async {}
/// Deprecated. Replaced by [AudioHandler.removeQueueItem].
@Deprecated("Use AudioHandler.removeQueueItem instead.")
Future<void> onRemoveQueueItem(MediaItem mediaItem) async {}
/// Deprecated. Replaced by [AudioHandler.skipToNext].
@Deprecated("Use AudioHandler.skipToNext instead.")
Future<void> onSkipToNext() => _skip(1);
/// Deprecated. Replaced by [AudioHandler.skipToPrevious].
@Deprecated("Use AudioHandler.skipToPrevious instead.")
Future<void> onSkipToPrevious() => _skip(-1);
/// Deprecated. Replaced by [AudioHandler.fastForward].
@Deprecated("Use AudioHandler.fastForward instead.")
Future<void> onFastForward() async {}
/// Deprecated. Replaced by [AudioHandler.rewind].
@Deprecated("Use AudioHandler.rewind instead.")
Future<void> onRewind() async {}
/// Deprecated. Replaced by [AudioHandler.skipToQueueItem].
@Deprecated("Use AudioHandler.skipToQueueItem instead.")
Future<void> onSkipToQueueItem(String mediaId) async {}
/// Deprecated. Replaced by [AudioHandler.seek].
@Deprecated("Use AudioHandler.seek instead.")
Future<void> onSeekTo(Duration position) async {}
/// Deprecated. Replaced by [AudioHandler.setRating].
@Deprecated("Use AudioHandler.setRating instead.")
Future<void> onSetRating(Rating rating, Map<String, dynamic>? extras) async {}
/// Deprecated. Replaced by [AudioHandler.setRepeatMode].
@Deprecated("Use AudioHandler.setRepeatMode instead.")
Future<void> onSetRepeatMode(AudioServiceRepeatMode repeatMode) async {}
/// Deprecated. Replaced by [AudioHandler.setShuffleMode].
@Deprecated("Use AudioHandler.setShuffleMode instead.")
Future<void> onSetShuffleMode(AudioServiceShuffleMode shuffleMode) async {}
/// Deprecated. Replaced by [AudioHandler.seekBackward].
@Deprecated("Use AudioHandler.seekBackward instead.")
Future<void> onSeekBackward(bool begin) async {}
/// Deprecated. Replaced by [AudioHandler.seekForward].
@Deprecated("Use AudioHandler.seekForward instead.")
Future<void> onSeekForward(bool begin) async {}
/// Deprecated. Replaced by [AudioHandler.setSpeed].
@Deprecated("Use AudioHandler.setSpeed instead.")
Future<void> onSetSpeed(double speed) async {}
/// Deprecated. Replaced by [AudioHandler.customAction].
@Deprecated("Use AudioHandler.customAction instead.")
Future<dynamic> onCustomAction(String name, dynamic arguments) async {}
/// Deprecated. Replaced by [AudioHandler.onTaskRemoved].
@Deprecated("Use AudioHandler.onTaskRemoved instead.")
Future<void> onTaskRemoved() async {}
/// Deprecated. Replaced by [AudioHandler.onNotificationDeleted].
@Deprecated("Use AudioHandler.onNotificationDeleted instead.")
Future<void> onClose() => onStop();
Future<void> _skip(int offset) async {
final mediaItem = _handler.mediaItem.nvalue;
if (mediaItem == null) return;
final queue = _handler.queue.nvalue ?? <MediaItem>[];
final i = queue.indexOf(mediaItem);
if (i == -1) return;
final newIndex = i + offset;
if (newIndex >= 0 && newIndex < queue.length) {
await onSkipToQueueItem(queue[newIndex].id);
}
}
}
/// An [AudioHandler] plays audio, provides state updates and query results to
/// clients. It implements standard protocols that allow it to be remotely
/// controlled by the lock screen, media notifications, the iOS control center,
/// headsets, smart watches, car audio systems, and other compatible agents.
///
/// This class cannot be subclassed directly. Implementations should subclass
/// [BaseAudioHandler], and composite behaviours should be defined as subclasses
/// of [CompositeAudioHandler].
abstract class AudioHandler {
AudioHandler._();
/// Prepare media items for playback.
Future<void> prepare();
/// Prepare a specific media item for playback.
Future<void> prepareFromMediaId(String mediaId,
[Map<String, dynamic>? extras]);
/// Prepare playback from a search query.
Future<void> prepareFromSearch(String query, [Map<String, dynamic>? extras]);
/// Prepare a media item represented by a Uri for playback.
Future<void> prepareFromUri(Uri uri, [Map<String, dynamic>? extras]);
/// Start or resume playback.
Future<void> play();
/// Play a specific media item.
Future<void> playFromMediaId(String mediaId, [Map<String, dynamic>? extras]);
/// Begin playback from a search query.
Future<void> playFromSearch(String query, [Map<String, dynamic>? extras]);
/// Play a media item represented by a Uri.
Future<void> playFromUri(Uri uri, [Map<String, dynamic>? extras]);
/// Play a specific media item.
Future<void> playMediaItem(MediaItem mediaItem);
/// Pause playback.
Future<void> pause();
/// Process a headset button click, where [button] defaults to
/// [MediaButton.media].
Future<void> click([MediaButton button = MediaButton.media]);
/// Stop playback and release resources.
Future<void> stop();
/// Add [mediaItem] to the queue.
Future<void> addQueueItem(MediaItem mediaItem);
/// Add [mediaItems] to the queue.
Future<void> addQueueItems(List<MediaItem> mediaItems);
/// Insert [mediaItem] into the queue at position [index].
Future<void> insertQueueItem(int index, MediaItem mediaItem);
/// Update to the queue to [queue].
Future<void> updateQueue(List<MediaItem> queue);
/// Update the properties of [mediaItem].
Future<void> updateMediaItem(MediaItem mediaItem);
/// Remove [mediaItem] from the queue.
Future<void> removeQueueItem(MediaItem mediaItem);
/// Remove media item from the queue at the specified [index].
Future<void> removeQueueItemAt(int index);
/// Skip to the next item in the queue.
Future<void> skipToNext();
/// Skip to the previous item in the queue.
Future<void> skipToPrevious();
/// Jump forward by [AudioServiceConfig.fastForwardInterval].
Future<void> fastForward();
/// Jump backward by [AudioServiceConfig.rewindInterval]. Note: this value
/// must be positive.
Future<void> rewind();
/// Skip to a queue item.
Future<void> skipToQueueItem(int index);
/// Seek to [position].
Future<void> seek(Duration position);
/// Set the rating.
Future<void> setRating(Rating rating, [Map<String, dynamic>? extras]);
/// Set whether captioning is enabled.
Future<void> setCaptioningEnabled(bool enabled);
/// Set the repeat mode.
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode);
/// Set the shuffle mode.
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode);
/// Begin or end seeking backward continuously.
Future<void> seekBackward(bool begin);
/// Begin or end seeking forward continuously.
Future<void> seekForward(bool begin);
/// Set the playback speed.
Future<void> setSpeed(double speed);
/// A mechanism to support app-specific actions.
Future<dynamic> customAction(String name, [Map<String, dynamic>? extras]);
/// Handle the task being swiped away in the task manager (Android).
Future<void> onTaskRemoved();
/// Handle the notification being swiped away (Android).
Future<void> onNotificationDeleted();
/// Get the children of a parent media item.
Future<List<MediaItem>> getChildren(String parentMediaId,
[Map<String, dynamic>? options]);
/// Get a value stream that emits service-specific options to send to the
/// client whenever the children under the specified parent change. The
/// emitted options may contain information about what changed. A client that
/// is subscribed to this stream should call [getChildren] to obtain the
/// changed children.
ValueStream<Map<String, dynamic>> subscribeToChildren(String parentMediaId);
/// Get a particular media item.
Future<MediaItem?> getMediaItem(String mediaId);
/// Search for media items.
Future<List<MediaItem>> search(String query, [Map<String, dynamic>? extras]);
/// Set the remote volume on Android. This works only when using
/// [RemoteAndroidPlaybackInfo].
Future<void> androidSetRemoteVolume(int volumeIndex);
/// Adjust the remote volume on Android. This works only when using
/// [RemoteAndroidPlaybackInfo].
Future<void> androidAdjustRemoteVolume(AndroidVolumeDirection direction);
/// A value stream of playback states.
ValueStream<PlaybackState> get playbackState;
/// A value stream of the current queue.
ValueStream<List<MediaItem>> get queue;
/// A value stream of the current queueTitle.
ValueStream<String> get queueTitle;
/// A value stream of the current media item.
ValueStream<MediaItem?> get mediaItem;
/// A value stream of the current rating style.
ValueStream<RatingStyle> get ratingStyle;
/// A value stream of the current [AndroidPlaybackInfo].
ValueStream<AndroidPlaybackInfo> get androidPlaybackInfo;
/// A stream of custom events.
Stream<dynamic> get customEvent;
/// A stream of custom states.
ValueStream<dynamic> get customState;
}
/// A [SwitchAudioHandler] wraps another [AudioHandler] that may be switched for
/// another at any time by setting [inner].
class SwitchAudioHandler extends CompositeAudioHandler {
final BehaviorSubject<PlaybackState> _playbackState = BehaviorSubject();
final BehaviorSubject<List<MediaItem>> _queue = BehaviorSubject();
final BehaviorSubject<String> _queueTitle = BehaviorSubject();
final BehaviorSubject<MediaItem?> _mediaItem = BehaviorSubject();
final BehaviorSubject<AndroidPlaybackInfo> _androidPlaybackInfo =
BehaviorSubject();
final BehaviorSubject<RatingStyle> _ratingStyle = BehaviorSubject();
final PublishSubject<dynamic> _customEvent = PublishSubject<dynamic>();
final BehaviorSubject<dynamic> _customState = BehaviorSubject<dynamic>();
@override
ValueStream<PlaybackState> get playbackState => _playbackState;
@override
ValueStream<List<MediaItem>> get queue => _queue;
@override
ValueStream<String> get queueTitle => _queueTitle;
@override
ValueStream<MediaItem?> get mediaItem => _mediaItem;
@override
ValueStream<AndroidPlaybackInfo> get androidPlaybackInfo =>
_androidPlaybackInfo;
@override
ValueStream<RatingStyle> get ratingStyle => _ratingStyle;
@override
Stream<dynamic> get customEvent => _customEvent;
@override
ValueStream<dynamic> get customState => _customState;
StreamSubscription<PlaybackState>? _playbackStateSubscription;
StreamSubscription<List<MediaItem>>? _queueSubscription;
StreamSubscription<String>? _queueTitleSubscription;
StreamSubscription<MediaItem?>? _mediaItemSubscription;
StreamSubscription<AndroidPlaybackInfo>? _androidPlaybackInfoSubscription;
StreamSubscription<RatingStyle>? _ratingStyleSubscription;
StreamSubscription<dynamic>? _customEventSubscription;
StreamSubscription<dynamic>? _customStateSubscription;
/// Creates a [SwitchAudioHandler] with an initial [inner] handler, which
/// defaults to a no-op handler.
SwitchAudioHandler([AudioHandler? inner])
: this._(inner ?? BaseAudioHandler());
SwitchAudioHandler._(AudioHandler inner) : super(inner) {
this.inner = inner;
}
/// The current inner [AudioHandler] that this [SwitchAudioHandler] will
/// delegate to.
AudioHandler get inner => _inner;
set inner(AudioHandler newInner) {
// Should disallow all ancestors...
assert(newInner != this);
_playbackStateSubscription?.cancel();
_queueSubscription?.cancel();
_queueTitleSubscription?.cancel();
_mediaItemSubscription?.cancel();
_androidPlaybackInfoSubscription?.cancel();
_ratingStyleSubscription?.cancel();
_customEventSubscription?.cancel();
_customStateSubscription?.cancel();
_inner = newInner;
_playbackStateSubscription = inner.playbackState.listen(_playbackState.add);
_queueSubscription = inner.queue.listen(_queue.add);
_queueTitleSubscription = inner.queueTitle.listen(_queueTitle.add);
// XXX: This only works in one direction.
_mediaItemSubscription = inner.mediaItem.listen(_mediaItem.add);
_androidPlaybackInfoSubscription =
inner.androidPlaybackInfo.listen(_androidPlaybackInfo.add);
_ratingStyleSubscription = inner.ratingStyle.listen(_ratingStyle.add);
_customEventSubscription = inner.customEvent.listen(_customEvent.add);
_customStateSubscription = inner.customState.listen(_customState.add);
}
}
/// A [CompositeAudioHandler] wraps another [AudioHandler] and adds additional
/// behaviour to it. Each method will by default pass through to the
/// corresponding method of the wrapped handler. If you override a method, it
/// must call super in addition to any "additional" functionality you add.
class CompositeAudioHandler extends AudioHandler {
AudioHandler _inner;
/// Create the [CompositeAudioHandler] with the given wrapped handler.
CompositeAudioHandler(AudioHandler inner)
: _inner = inner,
super._();
@override
@mustCallSuper
Future<void> prepare() => _inner.prepare();
@override
@mustCallSuper
Future<void> prepareFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) =>
_inner.prepareFromMediaId(mediaId, extras);
@override
@mustCallSuper
Future<void> prepareFromSearch(String query,
[Map<String, dynamic>? extras]) =>
_inner.prepareFromSearch(query, extras);
@override
@mustCallSuper
Future<void> prepareFromUri(Uri uri, [Map<String, dynamic>? extras]) =>
_inner.prepareFromUri(uri, extras);
@override
@mustCallSuper
Future<void> play() => _inner.play();
@override
@mustCallSuper
Future<void> playFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) =>
_inner.playFromMediaId(mediaId, extras);
@override
@mustCallSuper
Future<void> playFromSearch(String query, [Map<String, dynamic>? extras]) =>
_inner.playFromSearch(query, extras);
@override
@mustCallSuper
Future<void> playFromUri(Uri uri, [Map<String, dynamic>? extras]) =>
_inner.playFromUri(uri, extras);
@override
@mustCallSuper
Future<void> playMediaItem(MediaItem mediaItem) =>
_inner.playMediaItem(mediaItem);
@override
@mustCallSuper
Future<void> pause() => _inner.pause();
@override
@mustCallSuper
Future<void> click([MediaButton button = MediaButton.media]) =>
_inner.click(button);
@override
@mustCallSuper
Future<void> stop() => _inner.stop();
@override
@mustCallSuper
Future<void> addQueueItem(MediaItem mediaItem) =>
_inner.addQueueItem(mediaItem);
@override
@mustCallSuper
Future<void> addQueueItems(List<MediaItem> mediaItems) =>
_inner.addQueueItems(mediaItems);
@override
@mustCallSuper
Future<void> insertQueueItem(int index, MediaItem mediaItem) =>
_inner.insertQueueItem(index, mediaItem);
@override
@mustCallSuper
Future<void> updateQueue(List<MediaItem> queue) => _inner.updateQueue(queue);
@override
@mustCallSuper
Future<void> updateMediaItem(MediaItem mediaItem) =>
_inner.updateMediaItem(mediaItem);
@override
@mustCallSuper
Future<void> removeQueueItem(MediaItem mediaItem) =>
_inner.removeQueueItem(mediaItem);
@override
@mustCallSuper
Future<void> removeQueueItemAt(int index) => _inner.removeQueueItemAt(index);
@override
@mustCallSuper
Future<void> skipToNext() => _inner.skipToNext();
@override
@mustCallSuper
Future<void> skipToPrevious() => _inner.skipToPrevious();
@override
@mustCallSuper
Future<void> fastForward() => _inner.fastForward();
@override
@mustCallSuper
Future<void> rewind() => _inner.rewind();
@override
@mustCallSuper
Future<void> skipToQueueItem(int index) => _inner.skipToQueueItem(index);
@override
@mustCallSuper
Future<void> seek(Duration position) => _inner.seek(position);
@override
@mustCallSuper
Future<void> setRating(Rating rating, [Map<String, dynamic>? extras]) =>
_inner.setRating(rating, extras);
@override
@mustCallSuper
Future<void> setCaptioningEnabled(bool enabled) =>
_inner.setCaptioningEnabled(enabled);
@override
@mustCallSuper
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) =>
_inner.setRepeatMode(repeatMode);
@override
@mustCallSuper
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) =>
_inner.setShuffleMode(shuffleMode);
@override
@mustCallSuper
Future<void> seekBackward(bool begin) => _inner.seekBackward(begin);
@override
@mustCallSuper
Future<void> seekForward(bool begin) => _inner.seekForward(begin);
@override
@mustCallSuper
Future<void> setSpeed(double speed) => _inner.setSpeed(speed);
@override
@mustCallSuper
Future<dynamic> customAction(String name, [Map<String, dynamic>? extras]) =>
_inner.customAction(name, extras);
@override
@mustCallSuper
Future<void> onTaskRemoved() => _inner.onTaskRemoved();
@override
@mustCallSuper
Future<void> onNotificationDeleted() => _inner.onNotificationDeleted();
@override
@mustCallSuper
Future<List<MediaItem>> getChildren(String parentMediaId,
[Map<String, dynamic>? options]) =>
_inner.getChildren(parentMediaId, options);
@override
@mustCallSuper
ValueStream<Map<String, dynamic>> subscribeToChildren(String parentMediaId) =>
_inner.subscribeToChildren(parentMediaId);
@override
@mustCallSuper
Future<MediaItem?> getMediaItem(String mediaId) =>
_inner.getMediaItem(mediaId);
@override
@mustCallSuper
Future<List<MediaItem>> search(String query,
[Map<String, dynamic>? extras]) =>
_inner.search(query, extras);
@override
@mustCallSuper
Future<void> androidSetRemoteVolume(int volumeIndex) =>
_inner.androidSetRemoteVolume(volumeIndex);
@override
@mustCallSuper
Future<void> androidAdjustRemoteVolume(AndroidVolumeDirection direction) =>
_inner.androidAdjustRemoteVolume(direction);
@override
ValueStream<PlaybackState> get playbackState => _inner.playbackState;
@override
ValueStream<List<MediaItem>> get queue => _inner.queue;
@override
ValueStream<String> get queueTitle => _inner.queueTitle;
@override
ValueStream<MediaItem?> get mediaItem => _inner.mediaItem;
@override
ValueStream<RatingStyle> get ratingStyle => _inner.ratingStyle;
@override
ValueStream<AndroidPlaybackInfo> get androidPlaybackInfo =>
_inner.androidPlaybackInfo;
@override
Stream<dynamic> get customEvent => _inner.customEvent;
@override
ValueStream<dynamic> get customState => _inner.customState;
}
class _IsolateRequest {
/// The send port for sending the response of this request.
final SendPort sendPort;
final String method;
final List<dynamic>? arguments;
_IsolateRequest(this.sendPort, this.method, [this.arguments]);
}
/// A [CompositeAudioHandler] that can be accessed from other isolates via
/// [lookup].
///
/// This handler recognises the custom action 'unregister' so that
/// `customAction('unregister')` can be called from a client to unregister this
/// handler, or it can be unregistered by a direct invocation of [unregister].
class IsolatedAudioHandler extends CompositeAudioHandler {
/// The default port name by which this isolated audio handler can be looked
/// up.
static const defaultPortName = 'com.ryanheise.audioservice.port';
/// Connect to an [IsolatedAudioHandler] from another isolate having the name
/// [portName] (defaulting to [defaultPortName]).
static Future<AudioHandler> lookup(
{String portName = defaultPortName}) async {
assert(!kIsWeb, "Isolates are not supported on web");
final handler = _ClientIsolatedAudioHandler(portName: portName);
await handler._init();
return handler;
}
/// The port name to use when looking up this handler.
final String portName;
final _receivePort = ReceivePort();
/// Creates an [IsolatedAudioHandler] that can be looked up by [portName]
/// (defaulting to [defaultPortName]).
///
/// This will throw a [StateError] if another [IsolatedAudioHandler] was
/// already registered with the given port name. Setting [overridePortName] to
/// `true` will unregister any existing port name first. However, this is
/// inherently racy and may still throw the same [StateError] if another
/// isolate is able to register another new handler with the same name before
/// this isolate can.
IsolatedAudioHandler(
super.inner, {
this.portName = defaultPortName,
bool overridePortName = false,
}) : assert(!kIsWeb) {
_receivePort.listen((dynamic event) async {
final request = event as _IsolateRequest;
switch (request.method) {
case 'playbackState':
_syncStream(playbackState, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'queue':
_syncStream(queue, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'queueTitle':
_syncStream(queueTitle, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'mediaItem':
_syncStream(mediaItem, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'androidPlaybackInfo':
_syncStream(androidPlaybackInfo, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'ratingStyle':
_syncStream(ratingStyle, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'customEvent':
_syncStream<dynamic>(customEvent, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'customState':
_syncStream<dynamic>(customState, request.arguments![0] as SendPort);
request.sendPort.send(null);
break;
case 'prepare':
await prepare();
request.sendPort.send(null);
break;
case 'prepareFromMediaId':
await prepareFromMediaId(
request.arguments![0] as String,
request.arguments![1] as Map<String, dynamic>?,
);
request.sendPort.send(null);
break;
case 'prepareFromSearch':
await prepareFromSearch(
request.arguments![0] as String,
request.arguments![1] as Map<String, dynamic>?,
);
request.sendPort.send(null);
break;
case 'prepareFromUri':
await prepareFromUri(
request.arguments![0] as Uri,
request.arguments![1] as Map<String, dynamic>?,
);
request.sendPort.send(null);
break;
case 'play':
await play();
request.sendPort.send(null);
break;
case 'playFromMediaId':
await playFromMediaId(
request.arguments![0] as String,
request.arguments![1] as Map<String, dynamic>?,
);
request.sendPort.send(null);
break;
case 'playFromSearch':
await playFromSearch(
request.arguments![0] as String,
request.arguments![1] as Map<String, dynamic>?,
);
request.sendPort.send(null);
break;
case 'playFromUri':
await playFromUri(
request.arguments![0] as Uri,
request.arguments![1] as Map<String, dynamic>?,
);
request.sendPort.send(null);
break;
case 'playMediaItem':
await playMediaItem(request.arguments![0] as MediaItem);
request.sendPort.send(null);
break;
case 'pause':
await pause();
request.sendPort.send(null);
break;
case 'click':
await click(request.arguments![0] as MediaButton);
request.sendPort.send(null);
break;
case 'stop':
await stop();
request.sendPort.send(null);
break;
case 'addQueueItem':
await addQueueItem(request.arguments![0] as MediaItem);
request.sendPort.send(null);
break;
case 'addQueueItems':
await addQueueItems(request.arguments![0] as List<MediaItem>);
request.sendPort.send(null);
break;
case 'insertQueueItem':
await insertQueueItem(
request.arguments![0] as int,
request.arguments![1] as MediaItem,
);
request.sendPort.send(null);
break;
case 'updateQueue':
await updateQueue(request.arguments![0] as List<MediaItem>);
request.sendPort.send(null);
break;
case 'updateMediaItem':
await updateMediaItem(request.arguments![0] as MediaItem);
request.sendPort.send(null);
break;
case 'removeQueueItem':
await removeQueueItem(request.arguments![0] as MediaItem);
request.sendPort.send(null);
break;
case 'removeQueueItemAt':
await removeQueueItemAt(request.arguments![0] as int);
request.sendPort.send(null);
break;
case 'skipToNext':
await skipToNext();
request.sendPort.send(null);
break;
case 'skipToPrevious':
await skipToPrevious();
request.sendPort.send(null);
break;
case 'fastForward':
await fastForward();
request.sendPort.send(null);
break;
case 'rewind':
await rewind();
request.sendPort.send(null);
break;
case 'skipToQueueItem':
await skipToQueueItem(request.arguments![0] as int);
request.sendPort.send(null);
break;
case 'seek':
await seek(request.arguments![0] as Duration);
request.sendPort.send(null);
break;
case 'setRating':
await setRating(
request.arguments![0] as Rating,
request.arguments![1] as Map<String, dynamic>?,
);
request.sendPort.send(null);
break;
case 'setCaptioningEnabled':
await setCaptioningEnabled(request.arguments![0] as bool);
request.sendPort.send(null);
break;
case 'setRepeatMode':
await setRepeatMode(request.arguments![0] as AudioServiceRepeatMode);
request.sendPort.send(null);
break;
case 'setShuffleMode':
await setShuffleMode(
request.arguments![0] as AudioServiceShuffleMode);
request.sendPort.send(null);
break;
case 'seekBackward':
await seekBackward(request.arguments![0] as bool);
request.sendPort.send(null);
break;
case 'seekForward':
await seekForward(request.arguments![0] as bool);
request.sendPort.send(null);
break;
case 'setSpeed':
await setSpeed(request.arguments![0] as double);
request.sendPort.send(null);
break;
case 'customAction':
request.sendPort.send(await customAction(
request.arguments![0] as String,
request.arguments![1] as Map<String, dynamic>?,
));
break;
case 'onTaskRemoved':
await onTaskRemoved();
request.sendPort.send(null);
break;
case 'onNotificationDeleted':
await onNotificationDeleted();
request.sendPort.send(null);
break;
case 'getChildren':
request.sendPort.send(await getChildren(
request.arguments![0] as String,
request.arguments![1] as Map<String, dynamic>?,
));
break;
case 'subscribeToChildren':
final parentMediaId = request.arguments![0] as String;
final sendPort = request.arguments![1] as SendPort?;
subscribeToChildren(parentMediaId).listen(sendPort!.send);
break;
case 'getMediaItem':
final result = await getMediaItem(request.arguments![0] as String);
request.sendPort.send(result);
break;
case 'search':
request.sendPort.send(await search(
request.arguments![0] as String,
request.arguments![1] as Map<String, dynamic>?,
));
break;
case 'androidAdjustRemoteVolume':
await androidAdjustRemoteVolume(
request.arguments![0] as AndroidVolumeDirection);
request.sendPort.send(null);
break;
case 'androidSetRemoteVolume':
await androidSetRemoteVolume(request.arguments![0] as int);
request.sendPort.send(null);
break;
}
});
if (overridePortName) {
IsolateNameServer.removePortNameMapping(portName);
}
final success =
IsolateNameServer.registerPortWithName(_receivePort.sendPort, portName);
if (!success) {
throw StateError(
'Port name $portName is already registered by another IsolatedAudioHandler.');
}
}
/// Unregisters this handler so that it can no longer be looked up by
/// [portName].
void unregister() {
IsolateNameServer.removePortNameMapping(portName);
}
/// Forwards events from `stream` to the requesting isolate via [sendPort].
void _syncStream<T>(Stream<T> stream, SendPort sendPort) {
stream.listen(sendPort.send);
}
@override
Future<dynamic> customAction(String name,
[Map<String, dynamic>? extras]) async {
if (name == 'unregister') {
unregister();
} else {
return super.customAction(name, extras);
}
}
}
/// A proxy for an [IsolatedAudioHandler] running in another isolate.
///
/// All method invocations on this handler will be forwarded to the other
/// handler, and all stream events emitted by the other handler can be listened
/// to on this handler.
class _ClientIsolatedAudioHandler implements BaseAudioHandler {
final _childrenSubjects = <String, BehaviorSubject<Map<String, dynamic>>>{};
/// The port name of the [IsolatedAudioHandler] that this client handler
/// connects to.
final String portName;
@override
final BehaviorSubject<PlaybackState> playbackState = BehaviorSubject();
@override
final BehaviorSubject<List<MediaItem>> queue = BehaviorSubject();
@override
final BehaviorSubject<String> queueTitle = BehaviorSubject();
@override
final BehaviorSubject<MediaItem?> mediaItem = BehaviorSubject();
@override
final BehaviorSubject<AndroidPlaybackInfo> androidPlaybackInfo =
BehaviorSubject();
@override
final BehaviorSubject<RatingStyle> ratingStyle = BehaviorSubject();
@override
final PublishSubject<dynamic> customEvent = PublishSubject<dynamic>();
@override
final BehaviorSubject<dynamic> customState = BehaviorSubject<dynamic>();
_ClientIsolatedAudioHandler({
this.portName = IsolatedAudioHandler.defaultPortName,
});
Future<void> _init() async {
await _syncSubject(playbackState, 'playbackState');
await _syncSubject(queue, 'queue');
await _syncSubject(queueTitle, 'queueTitle');
await _syncSubject(mediaItem, 'mediaItem');
await _syncSubject(androidPlaybackInfo, 'androidPlaybackInfo');
await _syncSubject(ratingStyle, 'ratingStyle');
await _syncSubject<dynamic>(customEvent, 'customEvent');
await _syncSubject<dynamic>(customState, 'customState');
}
/// Opens a channel to the [IsolatedAudioSource] through which this proxy can
/// listen to events on a stream named [name] from that [IsolatedAudioSource]
/// and forward them on to this proxy's corresponding stream subject to
/// deliver to the client isolate.
Future<void> _syncSubject<T>(Subject<T> subject, String name) async {
final receivePort = ReceivePort();
receivePort.cast<T>().listen(subject.add);
await _send(name, <dynamic>[receivePort.sendPort]);
}
@override
Future<void> prepare() => _send('prepare');
@override
Future<void> prepareFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) =>
_send('prepareFromMediaId', <dynamic>[mediaId, extras]);
@override
Future<void> prepareFromSearch(String query,
[Map<String, dynamic>? extras]) =>
_send('prepareFromSearch', <dynamic>[query, extras]);
@override
Future<void> prepareFromUri(Uri uri, [Map<String, dynamic>? extras]) =>
_send('prepareFromUri', <dynamic>[uri, extras]);
@override
Future<void> play() => _send('play');
@override
Future<void> playFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) =>
_send('playFromMediaId', <dynamic>[mediaId, extras]);
@override
Future<void> playFromSearch(String query, [Map<String, dynamic>? extras]) =>
_send('playFromSearch', <dynamic>[query, extras]);
@override
Future<void> playFromUri(Uri uri, [Map<String, dynamic>? extras]) =>
_send('playFromUri', <dynamic>[uri, extras]);
@override
Future<void> playMediaItem(MediaItem mediaItem) =>
_send('playMediaItem', <dynamic>[mediaItem]);
@override
Future<void> pause() => _send('pause');
@override
Future<void> click([MediaButton button = MediaButton.media]) =>
_send('click', <dynamic>[button]);
@override
Future<void> stop() => _send('stop');
@override
Future<void> addQueueItem(MediaItem mediaItem) =>
_send('addQueueItem', <dynamic>[mediaItem]);
@override
Future<void> addQueueItems(List<MediaItem> mediaItems) =>
_send('addQueueItems', <dynamic>[mediaItems]);
@override
Future<void> insertQueueItem(int index, MediaItem mediaItem) =>
_send('insertQueueItem', <dynamic>[index, mediaItem]);
@override
Future<void> updateQueue(List<MediaItem> queue) =>
_send('updateQueue', <dynamic>[queue]);
@override
Future<void> updateMediaItem(MediaItem mediaItem) =>
_send('updateMediaItem', <dynamic>[mediaItem]);
@override
Future<void> removeQueueItem(MediaItem mediaItem) =>
_send('removeQueueItem', <dynamic>[mediaItem]);
@override
Future<void> removeQueueItemAt(int index) =>
_send('removeQueueItemAt', <dynamic>[index]);
@override
Future<void> skipToNext() => _send('skipToNext');
@override
Future<void> skipToPrevious() => _send('skipToPrevious');
@override
Future<void> fastForward() => _send('fastForward');
@override
Future<void> rewind() => _send('rewind');
@override
Future<void> skipToQueueItem(int index) =>
_send('skipToQueueItem', <dynamic>[index]);
@override
Future<void> seek(Duration position) => _send('seek', <dynamic>[position]);
@override
Future<void> setRating(Rating rating, [Map<String, dynamic>? extras]) =>
_send('setRating', <dynamic>[rating, extras]);
@override
Future<void> setCaptioningEnabled(bool enabled) =>
_send('setCaptioningEnabled', <dynamic>[enabled]);
@override
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) =>
_send('setRepeatMode', <dynamic>[repeatMode]);
@override
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) =>
_send('setShuffleMode', <dynamic>[shuffleMode]);
@override
Future<void> seekBackward(bool begin) =>
_send('seekBackward', <dynamic>[begin]);
@override
Future<void> seekForward(bool begin) =>
_send('seekForward', <dynamic>[begin]);
@override
Future<void> setSpeed(double speed) => _send('setSpeed', <dynamic>[speed]);
@override
Future<dynamic> customAction(String name, [Map<String, dynamic>? extras]) =>
_send('customAction', <dynamic>[name, extras]);
@override
Future<void> onTaskRemoved() => _send('onTaskRemoved');
@override
Future<void> onNotificationDeleted() => _send('onNotificationDeleted');
@override
Future<List<MediaItem>> getChildren(String parentMediaId,
[Map<String, dynamic>? options]) async =>
(await _send('getChildren', <dynamic>[parentMediaId, options]))
as List<MediaItem>;
@override
ValueStream<Map<String, dynamic>> subscribeToChildren(String parentMediaId) {
var childrenSubject = _childrenSubjects[parentMediaId];
if (childrenSubject == null) {
childrenSubject = _childrenSubjects[parentMediaId] = BehaviorSubject();
final receivePort = ReceivePort();
receivePort.listen((dynamic options) {
childrenSubject!.add(options as Map<String, dynamic>);
});
_send('subscribeToChildren',
<dynamic>[parentMediaId, receivePort.sendPort]);
}
return childrenSubject;
}
@override
Future<MediaItem?> getMediaItem(String mediaId) async =>
(await _send('getMediaItem', <dynamic>[mediaId])) as MediaItem?;
@override
Future<List<MediaItem>> search(String query,
[Map<String, dynamic>? extras]) async =>
(await _send('search', <dynamic>[query, extras])) as List<MediaItem>;
@override
Future<void> androidAdjustRemoteVolume(AndroidVolumeDirection direction) =>
_send('androidAdjustRemoteVolume', <dynamic>[direction]);
@override
Future<void> androidSetRemoteVolume(int volumeIndex) =>
_send('androidSetRemoteVolume', <dynamic>[volumeIndex]);
Future<dynamic> _send(String method, [List<dynamic>? arguments]) async {
final sendPort = IsolateNameServer.lookupPortByName(portName);
if (sendPort == null) {
throw StateError('IsolatedAudioHandler $portName not available');
}
final receivePort = ReceivePort();
sendPort.send(_IsolateRequest(receivePort.sendPort, method, arguments));
final dynamic result = await receivePort.first;
receivePort.close();
return result;
}
}
/// Base class for implementations of [AudioHandler]. It provides default
/// implementations of all methods and streams. Each stream in this class is
/// specialized as either a [BehaviorSubject] or [PublishSubject] providing an
/// additional `add` method for emitting values on those streams.
///
/// These are [BehaviorSubject]s provided by this class:
///
/// * [playbackState]
/// * [queue]
/// * [queueTitle]
/// * [androidPlaybackInfo]
/// * [ratingStyle]
///
/// Besides them, there's also [customEvent] which is a [PublishSubject].
///
/// You can choose to implement all methods yourself, or you may leverage some
/// mixins to provide default implementations of certain behaviours:
///
/// * [QueueHandler] provides default implementations of methods for updating
/// and navigating the queue.
/// * [SeekHandler] provides default implementations of methods for seeking
/// forwards and backwards.
///
/// ## Android service lifecycle and state transitions
///
/// On Android, the [AudioHandler] runs inside an Android service. This allows
/// the audio logic to continue running in the background, and also an app that
/// had previously been terminated to wake up and resume playing audio when the
/// user click on the play button in a media notification or headset.
///
/// ### Foreground/background transitions
///
/// The underlying Android service enters the `foreground` state whenever
/// [PlaybackState.playing] becomes `true`, and enters the `background` state
/// whenever [PlaybackState.playing] becomes `false`.
///
/// ### Start/stop transitions
///
/// The underlying Android service enters the `started` state whenever
/// [PlaybackState.playing] becomes `true`, and enters the `stopped` state
/// whenever [PlaybackState.processingState] becomes `idle`.
///
/// ### Create/destroy lifecycle
///
/// The underlying service is created either when a client binds to it, or when
/// it is started, and it is destroyed when no clients are bound to it AND it is
/// stopped. When the Flutter UI is attached to an Android Activity, this will
/// also bind to the service, and it will unbind from the service when the
/// Activity is destroyed. A media notification will also bind to the service.
///
/// If the service needs to be created when the app is not already running, your
/// app's `main` entrypoint will be called in the background which should
/// initialise your [AudioHandler].
class BaseAudioHandler extends AudioHandler {
/// A controller for broadcasting the current [PlaybackState] to the app's UI,
/// media notification and other clients. Example usage:
///
/// ```dart
/// playbackState.add(playbackState.value!.copyWith(playing: true));
/// ```
///
/// The state changes broadcast via this stream can be listened to via the
/// Flutter app's UI
@override
final BehaviorSubject<PlaybackState> playbackState =
BehaviorSubject.seeded(PlaybackState());
/// A controller for broadcasting the current queue to the app's UI, media
/// notification and other clients. Example usage:
///
/// ```dart
/// queue.add(queue.value! + [additionalItem]);
/// ```
@override
final BehaviorSubject<List<MediaItem>> queue =
BehaviorSubject.seeded(<MediaItem>[]);
/// A controller for broadcasting the current queue title to the app's UI, media
/// notification and other clients. Example usage:
///
/// ```dart
/// queueTitle.add(newTitle);
/// ```
@override
final BehaviorSubject<String> queueTitle = BehaviorSubject.seeded('');
/// A controller for broadcasting the current media item to the app's UI,
/// media notification and other clients. Example usage:
///
/// ```dart
/// mediaItem.add(item);
/// ```
@override
final BehaviorSubject<MediaItem?> mediaItem = BehaviorSubject.seeded(null);
/// A controller for broadcasting the current [AndroidPlaybackInfo] to the app's UI,
/// media notification and other clients. Example usage:
///
/// ```dart
/// androidPlaybackInfo.add(newPlaybackInfo);
/// ```
@override
final BehaviorSubject<AndroidPlaybackInfo> androidPlaybackInfo =
BehaviorSubject();
/// A controller for broadcasting the current rating style to the app's UI,
/// media notification and other clients. Example usage:
///
/// ```dart
/// ratingStyle.add(style);
/// ```
@override
final BehaviorSubject<RatingStyle> ratingStyle = BehaviorSubject();
/// A controller for broadcasting a custom event to the app's UI.
/// A shorthand for the event stream is [customEvent].
/// Example usage:
///
/// ```dart
/// customEventSubject.add(MyCustomEvent(arg: 3));
/// ```
@override
final PublishSubject<dynamic> customEvent = PublishSubject<dynamic>();
/// A controller for broadcasting the current custom state to the app's UI.
/// Example usage:
///
/// ```dart
/// customState.add(MyCustomState(...));
/// ```
@override
final BehaviorSubject<dynamic> customState = BehaviorSubject<dynamic>();
/// Constructor. Normally this is called from subclasses via `super`.
BaseAudioHandler() : super._();
@override
Future<void> prepare() async {}
@override
Future<void> prepareFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) async {}
@override
Future<void> prepareFromSearch(String query,
[Map<String, dynamic>? extras]) async {}
@override
Future<void> prepareFromUri(Uri uri, [Map<String, dynamic>? extras]) async {}
@override
Future<void> play() async {}
@override
Future<void> playFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) async {}
@override
Future<void> playFromSearch(String query,
[Map<String, dynamic>? extras]) async {}
@override
Future<void> playFromUri(Uri uri, [Map<String, dynamic>? extras]) async {}
@override
Future<void> playMediaItem(MediaItem mediaItem) async {}
@override
Future<void> pause() async {}
@override
Future<void> click([MediaButton button = MediaButton.media]) async {
switch (button) {
case MediaButton.media:
if (playbackState.nvalue?.playing == true) {
await pause();
} else {
await play();
}
break;
case MediaButton.next:
await skipToNext();
break;
case MediaButton.previous:
await skipToPrevious();
break;
}
}
/// Stop playback and release resources.
///
/// The default implementation (which may be overridden) updates
/// [playbackState] by setting the processing state to
/// [AudioProcessingState.idle] which disables the system notification.
@override
Future<void> stop() async {
playbackState.add(playbackState.nvalue!
.copyWith(processingState: AudioProcessingState.idle));
await playbackState.firstWhere(
(state) => state.processingState == AudioProcessingState.idle);
}
@override
Future<void> addQueueItem(MediaItem mediaItem) async {}
@override
Future<void> addQueueItems(List<MediaItem> mediaItems) async {}
@override
Future<void> insertQueueItem(int index, MediaItem mediaItem) async {}
@override
Future<void> updateQueue(List<MediaItem> queue) async {}
@override
Future<void> updateMediaItem(MediaItem mediaItem) async {}
@override
Future<void> removeQueueItem(MediaItem mediaItem) async {}
@override
Future<void> removeQueueItemAt(int index) async {}
@override
Future<void> skipToNext() async {}
@override
Future<void> skipToPrevious() async {}
@override
Future<void> fastForward() async {}
@override
Future<void> rewind() async {}
@override
Future<void> skipToQueueItem(int index) async {}
@override
Future<void> seek(Duration position) async {}
@override
Future<void> setRating(Rating rating, [Map<String, dynamic>? extras]) async {}
@override
Future<void> setCaptioningEnabled(bool enabled) async {}
@override
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {}
@override
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {}
@override
Future<void> seekBackward(bool begin) async {}
@override
Future<void> seekForward(bool begin) async {}
@override
Future<void> setSpeed(double speed) async {}
@override
Future<dynamic> customAction(String name,
[Map<String, dynamic>? extras]) async {}
@override
Future<void> onTaskRemoved() async {}
@override
Future<void> onNotificationDeleted() async {
await stop();
}
@override
Future<List<MediaItem>> getChildren(String parentMediaId,
[Map<String, dynamic>? options]) async =>
[];
@override
ValueStream<Map<String, dynamic>> subscribeToChildren(String parentMediaId) =>
BehaviorSubject.seeded(<String, dynamic>{});
@override
Future<MediaItem?> getMediaItem(String mediaId) async => null;
@override
Future<List<MediaItem>> search(String query,
[Map<String, dynamic>? extras]) async =>
[];
@override
Future<void> androidAdjustRemoteVolume(
AndroidVolumeDirection direction) async {}
@override
Future<void> androidSetRemoteVolume(int volumeIndex) async {}
}
/// This mixin provides default implementations of [fastForward], [rewind],
/// [seekForward] and [seekBackward] which are all defined in terms of your own
/// implementation of [seek].
mixin SeekHandler on BaseAudioHandler {
_Seeker? _seeker;
@override
Future<void> fastForward() =>
_seekRelative(AudioService.config.fastForwardInterval);
@override
Future<void> rewind() => _seekRelative(-AudioService.config.rewindInterval);
@override
Future<void> seekForward(bool begin) async => _seekContinuously(begin, 1);
@override
Future<void> seekBackward(bool begin) async => _seekContinuously(begin, -1);
/// Jumps away from the current position by [offset].
Future<void> _seekRelative(Duration offset) async {
var newPosition = playbackState.nvalue!.position + offset;
// Make sure we don't jump out of bounds.
if (newPosition < Duration.zero) {
newPosition = Duration.zero;
}
final duration = mediaItem.nvalue?.duration ?? Duration.zero;
if (newPosition > duration) {
newPosition = duration;
}
// Perform the jump via a seek.
await seek(newPosition);
}
/// Begins or stops a continuous seek in [direction]. After it begins it will
/// continue seeking forward or backward by 10 seconds within the audio, at
/// intervals of 1 second in app time.
void _seekContinuously(bool begin, int direction) {
_seeker?.stop();
if (begin && mediaItem.nvalue?.duration != null) {
_seeker = _Seeker(this, Duration(seconds: 10 * direction),
const Duration(seconds: 1), mediaItem.nvalue!.duration!)
..start();
}
}
}
class _Seeker {
final AudioHandler handler;
final Duration positionInterval;
final Duration stepInterval;
final Duration duration;
bool _running = false;
_Seeker(
this.handler,
this.positionInterval,
this.stepInterval,
this.duration,
);
Future<void> start() async {
_running = true;
while (_running) {
var newPosition =
handler.playbackState.nvalue!.position + positionInterval;
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > duration) newPosition = duration;
handler.seek(newPosition);
await Future<void>.delayed(stepInterval);
}
}
void stop() {
_running = false;
}
}
/// This mixin provides default implementations of methods for updating and
/// navigating the queue. When using this mixin, you must add a list of
/// [MediaItem]s to [queue], override [skipToQueueItem] and initialise the queue
/// index (e.g. by calling [skipToQueueItem] with the initial queue index). The
/// [skipToNext] and [skipToPrevious] default implementations are defined by
/// this mixin in terms of your own implementation of [skipToQueueItem].
mixin QueueHandler on BaseAudioHandler {
@override
Future<void> addQueueItem(MediaItem mediaItem) async {
queue.add(queue.nvalue!..add(mediaItem));
await super.addQueueItem(mediaItem);
}
@override
Future<void> addQueueItems(List<MediaItem> mediaItems) async {
queue.add(queue.nvalue!..addAll(mediaItems));
await super.addQueueItems(mediaItems);
}
@override
Future<void> insertQueueItem(int index, MediaItem mediaItem) async {
queue.add(queue.nvalue!..insert(index, mediaItem));
await super.insertQueueItem(index, mediaItem);
}
@override
Future<void> updateQueue(List<MediaItem> queue) async {
this.queue.add(
this.queue.nvalue!..replaceRange(0, this.queue.nvalue!.length, queue));
await super.updateQueue(queue);
}
@override
Future<void> updateMediaItem(MediaItem mediaItem) async {
queue.add(queue.nvalue!..[queue.nvalue!.indexOf(mediaItem)] = mediaItem);
await super.updateMediaItem(mediaItem);
}
@override
Future<void> removeQueueItem(MediaItem mediaItem) async {
queue.add(queue.nvalue!..remove(mediaItem));
await super.removeQueueItem(mediaItem);
}
@override
Future<void> skipToNext() async {
await _skip(1);
await super.skipToNext();
}
@override
Future<void> skipToPrevious() async {
await _skip(-1);
await super.skipToPrevious();
}
/// This should be overridden to skip to the queue item at [index].
/// Implementations should broadcast the new queue index via [playbackState],
/// broadcast the new media item via [mediaItem], and potentially issue
/// instructions to start the new item playing. Some implementations may
/// choose to automatically play when skipping to a queue item while others
/// may prefer to play the new item only if the player was already playing
/// another item beforehand.
///
/// An example implementation may look like:
///
/// ```dart
/// playbackState.add(playbackState.value!.copyWith(queueIndex: index));
/// mediaItem.add(queue.value![index]);
/// player.playAtIndex(index); // use your player's respective API
/// await super.skipToQueueItem(index);
/// ```
@override
Future<void> skipToQueueItem(int index) async {
await super.skipToQueueItem(index);
}
Future<void> _skip(int offset) async {
final queue = this.queue.nvalue!;
final index = playbackState.nvalue!.queueIndex!;
if (index < 0 || index >= queue.length) return;
return skipToQueueItem(index + offset);
}
}
/// The available shuffle modes for the queue.
enum AudioServiceShuffleMode {
/// The queue will not be shuffled.
none,
/// The whole queue will be shuffled.
all,
/// A group of items will be shuffled. This corresponds to Android's
/// [SHUFFLE_MODE_GROUP](https://developer.android.com/reference/androidx/media2/common/SessionPlayer#SHUFFLE_MODE_GROUP).
group,
}
/// The available repeat modes.
///
/// This defines how media items should repeat when the current one is finished.
enum AudioServiceRepeatMode {
/// The current media item or queue will not repeat.
none,
/// The current media item will repeat.
one,
/// Playback will continue looping through all media items in the current list.
all,
/// UNIMPLEMENTED - see https://github.com/ryanheise/audio_service/issues/560
///
/// This corresponds to Android's [REPEAT_MODE_GROUP](https://developer.android.com/reference/androidx/media2/common/SessionPlayer#REPEAT_MODE_GROUP).
///
/// This could represent a playlist that is a smaller subset of all media items.
group,
}
/// The configuration options to use when intializing the [AudioService].
class AudioServiceConfig {
/// Whether on Android a media button click wakes up the media session and
/// resumes playback.
// TODO: either fix, or remove this https://github.com/ryanheise/audio_service/issues/638
final bool androidResumeOnClick;
/// The ID of the media notification channel. This will default to
/// `<YOUR_PACKAGE_NAME>.channel` where `<YOUR_PACKAGE_NAME>` is your app's
/// package name. e.g. `com.mycompany.myapp.channel`.
///
/// If your app uses multiple notification channels, make sure each channel
/// has a unique ID so that they don't clash. It is recommended to override
/// the default ID.
///
/// NOTE: After a user installs and runs your app, a channel will be created
/// with this ID and will show up in the app's settings. If you subsequently
/// change this channel ID here, it will result in a new channel being created
/// under the new ID leaving the old channel still visible. Therefore, if your
/// app has already been published, you might prefer to keep using the same
/// channel ID that you are currently using.
final String? androidNotificationChannelId;
/// The name of the media notification channel, that is visible to user in
/// settings of your app.
final String androidNotificationChannelName;
/// A description of the media notification channel, that is visible to user
/// in settings of your app.
final String? androidNotificationChannelDescription;
/// The color to use on the background of the notification on Android. This
/// should be a non-transparent color.
final Color? notificationColor;
/// The icon resource to be used in the Android media notification, specified
/// like an XML resource reference. This should be a monochrome white icon on
/// a transparent background. The default value is `"mipmap/ic_launcher"`.
final String androidNotificationIcon;
/// Whether notification badges (also known as notification dots) should
/// appear on a launcher icon when the app has an active notification.
final bool androidShowNotificationBadge;
/// Whether the application activity will be opened on click on notification.
final bool androidNotificationClickStartsActivity;
/// Whether the notification can be swiped away.
///
/// If you set this to true, [androidStopForegroundOnPause] must be true as well,
/// otherwise this will not do anything, because when foreground service is active,
/// it forces notification to be ongoing.
final bool androidNotificationOngoing;
/// Whether the Android service should switch to a lower priority state when
/// playback is paused allowing the user to swipe away the notification. Note
/// that while in this lower priority state, the operating system will also be
/// able to kill your service at any time to reclaim resources.
final bool androidStopForegroundOnPause;
/// If not null, causes the artwork specified by [MediaItem.artUri] to be
/// downscaled to this maximum pixel width. If the resolution of your artwork
/// is particularly high, this can help to conserve memory. If specified,
/// [artDownscaleHeight] must also be specified.
final int? artDownscaleWidth;
/// If not null, causes the artwork specified by [MediaItem.artUri] to be
/// downscaled to this maximum pixel height. If the resolution of your artwork
/// is particularly high, this can help to conserve memory. If specified,
/// [artDownscaleWidth] must also be specified.
final int? artDownscaleHeight;
/// The interval to be used in [AudioHandler.fastForward]. This value will
/// also be used on iOS to render the skip-forward button. This value must be
/// positive.
final Duration fastForwardInterval;
/// The interval to be used in [AudioHandler.rewind]. This value will also be
/// used on iOS to render the skip-backward button. This value must be
/// positive.
final Duration rewindInterval;
/// By default artworks are loaded only when the item is fed into [AudioHandler.mediaItem].
///
/// If set to `true`, artworks for items start loading as soon as they are added to
/// [AudioHandler.queue].
final bool preloadArtwork;
/// Extras to report on Android in response to an `onGetRoot` request.
final Map<String, dynamic>? androidBrowsableRootExtras;
/// Creates a configuration object.
const AudioServiceConfig({
this.androidResumeOnClick = true,
this.androidNotificationChannelId,
this.androidNotificationChannelName = 'Notifications',
this.androidNotificationChannelDescription,
this.notificationColor,
this.androidNotificationIcon = 'mipmap/ic_launcher',
this.androidShowNotificationBadge = false,
this.androidNotificationClickStartsActivity = true,
this.androidNotificationOngoing = false,
this.androidStopForegroundOnPause = true,
this.artDownscaleWidth,
this.artDownscaleHeight,
this.fastForwardInterval = const Duration(seconds: 10),
this.rewindInterval = const Duration(seconds: 10),
this.preloadArtwork = false,
this.androidBrowsableRootExtras,
}) : assert((artDownscaleWidth != null) == (artDownscaleHeight != null)),
assert(
!androidNotificationOngoing || androidStopForegroundOnPause,
'The androidNotificationOngoing will make no effect with androidStopForegroundOnPause set to false',
);
AudioServiceConfigMessage _toMessage() => AudioServiceConfigMessage(
androidResumeOnClick: androidResumeOnClick,
androidNotificationChannelId: androidNotificationChannelId,
androidNotificationChannelName: androidNotificationChannelName,
androidNotificationChannelDescription:
androidNotificationChannelDescription,
notificationColor: notificationColor,
androidNotificationIcon: androidNotificationIcon,
androidShowNotificationBadge: androidShowNotificationBadge,
androidNotificationClickStartsActivity:
androidNotificationClickStartsActivity,
androidNotificationOngoing: androidNotificationOngoing,
androidStopForegroundOnPause: androidStopForegroundOnPause,
artDownscaleWidth: artDownscaleWidth,
artDownscaleHeight: artDownscaleHeight,
fastForwardInterval: fastForwardInterval,
rewindInterval: rewindInterval,
preloadArtwork: preloadArtwork,
androidBrowsableRootExtras: androidBrowsableRootExtras,
);
@override
String toString() => '${_toMessage().toMap()}';
}
/// Key/value codes for use in [MediaItem.extras] and
/// [AudioServiceConfig.androidBrowsableRootExtras] to influence how Android
/// Auto will style browsable and playable media items.
class AndroidContentStyle {
/// Set this key to `true` in [AudioServiceConfig.androidBrowsableRootExtras]
/// to declare that content style is supported.
static const supportedKey = 'android.media.browse.CONTENT_STYLE_SUPPORTED';
/// The key in [MediaItem.extras] and
/// [AudioServiceConfig.androidBrowsableRootExtras] to configure the content
/// style for playable items. The value can be any of the `*ItemHintValue`
/// constants defined in this class.
static const playableHintKey =
'android.media.browse.CONTENT_STYLE_PLAYABLE_HINT';
/// The key in [MediaItem.extras] and
/// [AudioServiceConfig.androidBrowsableRootExtras] to configure the content
/// style for browsable items. The value can be any of the `*ItemHintValue`
/// constants defined in this class.
static const browsableHintKey =
'android.media.browse.CONTENT_STYLE_BROWSABLE_HINT';
/// Specifies that items should be presented as lists.
static const listItemHintValue = 1;
/// Specifies that items should be presented as grids.
static const gridItemHintValue = 2;
/// Specifies that items should be presented as lists with vector icons.
static const categoryListItemHintValue = 3;
/// Specifies that items should be presented as grids with vector icons.
static const categoryGridItemHintValue = 4;
}
/// (Maybe) temporary.
extension AudioServiceValueStream<T> on ValueStream<T> {
/// Returns `this`.
@Deprecated('Use "this" instead. Will be removed before the release')
ValueStream<T> get stream => this;
}
extension _MediaItemMessageExtension on MediaItemMessage {
MediaItem toPlugin() => MediaItem(
id: id,
album: album,
title: title,
artist: artist,
genre: genre,
duration: duration,
artUri: artUri,
playable: playable,
displayTitle: displayTitle,
displaySubtitle: displaySubtitle,
displayDescription: displayDescription,
rating: rating?.toPlugin(),
isLive: isLive,
extras: extras,
);
}
extension _RatingMessageExtension on RatingMessage {
Rating toPlugin() => Rating._(RatingStyle.values[type.index], value);
}
extension _AndroidVolumeDirectionMessageExtension
on AndroidVolumeDirectionMessage {
AndroidVolumeDirection toPlugin() => AndroidVolumeDirection.values[index]!;
}
extension _MediaButtonMessageExtension on MediaButtonMessage {
MediaButton toPlugin() => MediaButton.values[index];
}
/// An enum of volume direction controls on Android.
class AndroidVolumeDirection {
/// Lower the ringer volume.
static final lower = AndroidVolumeDirection._(-1);
/// Keep the previous ringer volume.
static final same = AndroidVolumeDirection._(0);
/// Raise the ringer volume.
static final raise = AndroidVolumeDirection._(1);
/// A map of indices to values.
static final values = <int, AndroidVolumeDirection>{
-1: lower,
0: same,
1: raise,
};
/// The index for this enum value.
final int index;
AndroidVolumeDirection._(this.index);
@override
String toString() => '$index';
}
/// An enumeration of different volume control types on Android.
enum AndroidVolumeControlType {
/// The volume cannot be changed.
fixed,
/// The volume can be adjusted relatively.
relative,
/// The volume can be set using an absolute value.
absolute,
}
/// Information about volume control for either local or remote playback
/// depending on the subclass.
abstract class AndroidPlaybackInfo {
AndroidPlaybackInfoMessage _toMessage();
@override
String toString() => '${_toMessage().toMap()}';
}
/// Playback information for remote volume handling.
class RemoteAndroidPlaybackInfo extends AndroidPlaybackInfo {
//final AndroidAudioAttributes audioAttributes;
/// The type of volume control supported by the session.
final AndroidVolumeControlType volumeControlType;
/// The maximum volume supported.
final int maxVolume;
/// The current volume.
final int volume;
// ignore: public_member_api_docs
RemoteAndroidPlaybackInfo({
required this.volumeControlType,
required this.maxVolume,
required this.volume,
});
/// Creates a copy of this object with fields replaced.
AndroidPlaybackInfo copyWith({
AndroidVolumeControlType? volumeControlType,
int? maxVolume,
int? volume,
}) =>
RemoteAndroidPlaybackInfo(
volumeControlType: volumeControlType ?? this.volumeControlType,
maxVolume: maxVolume ?? this.maxVolume,
volume: volume ?? this.volume,
);
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is RemoteAndroidPlaybackInfo &&
volumeControlType == other.volumeControlType &&
maxVolume == other.maxVolume &&
volume == other.volume;
@override
int get hashCode => Object.hash(volumeControlType, maxVolume, volume);
@override
RemoteAndroidPlaybackInfoMessage _toMessage() =>
RemoteAndroidPlaybackInfoMessage(
volumeControlType:
AndroidVolumeControlTypeMessage.values[volumeControlType.index],
maxVolume: maxVolume,
volume: volume,
);
}
/// Playback information for local volume handling.
class LocalAndroidPlaybackInfo extends AndroidPlaybackInfo {
@override
bool operator ==(Object other) => other.runtimeType == runtimeType;
@override
int get hashCode => 0;
@override
LocalAndroidPlaybackInfoMessage _toMessage() =>
const LocalAndroidPlaybackInfoMessage();
}
/// This class is deprecated. Use the stream subjects in [BaseAudioHandler]
/// instead.
@Deprecated("Use stream subjects in BaseAudioHandler instead.")
class AudioServiceBackground {
static SwitchAudioHandler get _handler =>
AudioService._handler as SwitchAudioHandler;
static Completer<BackgroundAudioTask>? _startCompleter;
/// Deprecated. Use [AudioHandler.playbackState] instead.
@Deprecated("Use AudioHandler.playbackState instead.")
static PlaybackState get state =>
_handler.playbackState.nvalue ?? PlaybackState();
/// Deprecated. Use [AudioHandler.queue] instead.
@Deprecated("Use AudioHandler.queue instead.")
static List<MediaItem>? get queue => _handler.queue.nvalue;
/// Deprecated. Use [AudioService.init] instead.
@Deprecated("Use AudioService.init instead")
static Future<void> run(BackgroundAudioTask Function() taskBuilder) async {
final task = taskBuilder();
_startCompleter!.complete(task);
}
/// Deprecated. Use [BaseAudioHandler.playbackState] instead.
@Deprecated("Use BaseAudioHandler.playbackState instead.")
static Future<void> setState({
List<MediaControl>? controls,
List<MediaAction>? systemActions,
AudioProcessingState? processingState,
bool? playing,
Duration? position,
Duration? bufferedPosition,
double? speed,
DateTime? updateTime,
List<int>? androidCompactActions,
AudioServiceRepeatMode? repeatMode,
AudioServiceShuffleMode? shuffleMode,
}) async {
final oldState = _handler.playbackState.nvalue!;
_taskHandler.playbackState.add(PlaybackState(
controls: controls ?? oldState.controls,
systemActions: systemActions?.toSet() ?? oldState.systemActions,
processingState: processingState ?? oldState.processingState,
playing: playing ?? oldState.playing,
updatePosition: position ?? oldState.position,
bufferedPosition: bufferedPosition ?? oldState.bufferedPosition,
speed: speed ?? oldState.speed,
androidCompactActionIndices:
androidCompactActions ?? oldState.androidCompactActionIndices,
repeatMode: repeatMode ?? oldState.repeatMode,
shuffleMode: shuffleMode ?? oldState.shuffleMode,
));
}
static _BackgroundAudioHandler get _taskHandler =>
_handler.inner as _BackgroundAudioHandler;
/// Deprecated. Use [BaseAudioHandler.queue] instead.
@Deprecated("Use BaseAudioHandler.queue instead.")
static Future<void> setQueue(List<MediaItem> queue,
{bool preloadArtwork = false}) async {
if (preloadArtwork) {
// ignore: avoid_print
print(
'WARNING: preloadArtwork is not enabled! '
'This is deprecated and must be set via AudioService.init()',
);
}
_taskHandler.queue.add(queue);
}
/// Deprecated. Use [BaseAudioHandler.mediaItem] instead.
@Deprecated("Use BaseAudioHandler.mediaItem instead.")
static Future<void> setMediaItem(MediaItem mediaItem) async {
_taskHandler.mediaItem.add(mediaItem);
}
/// Deprecated. Use [AudioHandler.subscribeToChildren] instead.
@Deprecated("Use AudioHandler.subscribeToChildren instead.")
static Future<void> notifyChildrenChanged(
[String parentMediaId = AudioService.browsableRootId]) async {
await _platform.notifyChildrenChanged(
NotifyChildrenChangedRequest(parentMediaId: parentMediaId));
}
/// Deprecated. Use [AudioService.androidForceEnableMediaButtons] instead.
@Deprecated("Use AudioService.androidForceEnableMediaButtons instead.")
static Future<void> androidForceEnableMediaButtons() async {
await AudioService.androidForceEnableMediaButtons();
}
/// Deprecated. Use [BaseAudioHandler.customEvent] instead.
@Deprecated("Use BaseAudioHandler.customEvent instead.")
static void sendCustomEvent(dynamic event) {
_taskHandler.customEvent.add(event);
}
}
class _HandlerCallbacks extends AudioHandlerCallbacks {
final _handlerCompleter = Completer<AudioHandler>();
Future<AudioHandler> get handlerFuture => _handlerCompleter.future;
void setHandler(AudioHandler handler) => _handlerCompleter.complete(handler);
@override
Future<void> addQueueItem(AddQueueItemRequest request) async =>
(await handlerFuture).addQueueItem(request.mediaItem.toPlugin());
@override
Future<void> androidAdjustRemoteVolume(
AndroidAdjustRemoteVolumeRequest request) async =>
(await handlerFuture)
.androidAdjustRemoteVolume(request.direction.toPlugin());
@override
Future<void> androidSetRemoteVolume(
AndroidSetRemoteVolumeRequest request) async =>
(await handlerFuture).androidSetRemoteVolume(request.volumeIndex);
@override
Future<void> click(ClickRequest request) async {
return (await handlerFuture).click(request.button.toPlugin());
}
@override
Future<dynamic> customAction(CustomActionRequest request) async =>
(await handlerFuture).customAction(request.name, request.extras);
@override
Future<void> fastForward(FastForwardRequest request) async =>
(await handlerFuture).fastForward();
@override
Future<GetChildrenResponse> getChildren(GetChildrenRequest request) async {
final mediaItems =
await _onLoadChildren(request.parentMediaId, request.options);
return GetChildrenResponse(
children: mediaItems.map((item) => item._toMessage()).toList());
}
@override
Future<GetMediaItemResponse> getMediaItem(GetMediaItemRequest request) async {
return GetMediaItemResponse(
mediaItem: (await (await handlerFuture).getMediaItem(request.mediaId))
?._toMessage());
}
@override
Future<void> insertQueueItem(InsertQueueItemRequest request) async =>
(await handlerFuture)
.insertQueueItem(request.index, request.mediaItem.toPlugin());
@override
Future<void> onNotificationClicked(
OnNotificationClickedRequest request) async {
AudioService._notificationClicked.add(request.clicked);
}
@override
Future<void> onNotificationDeleted(
OnNotificationDeletedRequest request) async =>
(await handlerFuture).onNotificationDeleted();
@override
Future<void> onTaskRemoved(OnTaskRemovedRequest request) async =>
(await handlerFuture).onTaskRemoved();
@override
Future<void> pause(PauseRequest request) async =>
(await handlerFuture).pause();
@override
Future<void> play(PlayRequest request) async => (await handlerFuture).play();
@override
Future<void> playFromMediaId(PlayFromMediaIdRequest request) async =>
(await handlerFuture).playFromMediaId(request.mediaId, request.extras);
@override
Future<void> playFromSearch(PlayFromSearchRequest request) async =>
(await handlerFuture).playFromSearch(request.query, request.extras);
@override
Future<void> playFromUri(PlayFromUriRequest request) async =>
(await handlerFuture).playFromUri(request.uri, request.extras);
@override
Future<void> playMediaItem(PlayMediaItemRequest request) async =>
(await handlerFuture).playMediaItem(request.mediaItem.toPlugin());
@override
Future<void> prepare(PrepareRequest request) async =>
(await handlerFuture).prepare();
@override
Future<void> prepareFromMediaId(PrepareFromMediaIdRequest request) async =>
(await handlerFuture).prepareFromMediaId(request.mediaId, request.extras);
@override
Future<void> prepareFromSearch(PrepareFromSearchRequest request) async =>
(await handlerFuture).prepareFromSearch(request.query, request.extras);
@override
Future<void> prepareFromUri(PrepareFromUriRequest request) async =>
(await handlerFuture).prepareFromUri(request.uri, request.extras);
@override
Future<void> removeQueueItem(RemoveQueueItemRequest request) async =>
(await handlerFuture).removeQueueItem(request.mediaItem.toPlugin());
@override
Future<void> removeQueueItemAt(RemoveQueueItemAtRequest request) async =>
(await handlerFuture).removeQueueItemAt(request.index);
@override
Future<void> rewind(RewindRequest request) async =>
(await handlerFuture).rewind();
@override
Future<SearchResponse> search(SearchRequest request) async => SearchResponse(
mediaItems:
(await (await handlerFuture).search(request.query, request.extras))
.map((item) => item._toMessage())
.toList());
@override
Future<void> seek(SeekRequest request) async =>
(await handlerFuture).seek(request.position);
@override
Future<void> seekBackward(SeekBackwardRequest request) async =>
(await handlerFuture).seekBackward(request.begin);
@override
Future<void> seekForward(SeekForwardRequest request) async =>
(await handlerFuture).seekForward(request.begin);
@override
Future<void> setCaptioningEnabled(
SetCaptioningEnabledRequest request) async =>
(await handlerFuture).setCaptioningEnabled(request.enabled);
@override
Future<void> setRating(SetRatingRequest request) async =>
(await handlerFuture)
.setRating(request.rating.toPlugin(), request.extras);
@override
Future<void> setRepeatMode(SetRepeatModeRequest request) async =>
(await handlerFuture).setRepeatMode(
AudioServiceRepeatMode.values[request.repeatMode.index]);
@override
Future<void> setShuffleMode(SetShuffleModeRequest request) async =>
(await handlerFuture).setShuffleMode(
AudioServiceShuffleMode.values[request.shuffleMode.index]);
@override
Future<void> setSpeed(SetSpeedRequest request) async =>
(await handlerFuture).setSpeed(request.speed);
@override
Future<void> skipToNext(SkipToNextRequest request) async =>
(await handlerFuture).skipToNext();
@override
Future<void> skipToPrevious(SkipToPreviousRequest request) async =>
(await handlerFuture).skipToPrevious();
@override
Future<void> skipToQueueItem(SkipToQueueItemRequest request) async =>
(await handlerFuture).skipToQueueItem(request.index);
@override
Future<void> stop(StopRequest request) async => (await handlerFuture).stop();
final Map<String, ValueStream<Map<String, dynamic>>> _childrenSubscriptions =
{};
Future<List<MediaItem>> _onLoadChildren(
String parentMediaId, Map<String, dynamic>? options) async {
var childrenSubscription = _childrenSubscriptions[parentMediaId];
if (childrenSubscription == null) {
childrenSubscription = _childrenSubscriptions[parentMediaId] =
(await handlerFuture).subscribeToChildren(parentMediaId);
childrenSubscription.listen((Map<String, dynamic>? options) {
// Notify clients that the children of [parentMediaId] have changed.
_platform.notifyChildrenChanged(NotifyChildrenChangedRequest(
parentMediaId: parentMediaId,
options: options,
));
});
}
return await (await handlerFuture).getChildren(parentMediaId, options);
}
}
/// Backwards compatible extensions on rxdart's ValueStream
extension _ValueStreamExtension<T> on ValueStream<T> {
/// Backwards compatible version of valueOrNull.
T? get nvalue => hasValue ? value : null;
}
/// This widget is no longer required and has been deprecated.
@Deprecated("This widget is no longer required and can be safely removed.")
class AudioServiceWidget extends StatelessWidget {
/// Deprecated.
final Widget child;
/// Deprecated.
const AudioServiceWidget({super.key, required this.child});
@override
Widget build(BuildContext context) {
return child;
}
}