diff --git a/audio_service/example/lib/main.dart b/audio_service/example/lib/main.dart index 7c624e24..5d9b4ad5 100644 --- a/audio_service/example/lib/main.dart +++ b/audio_service/example/lib/main.dart @@ -636,7 +636,7 @@ class LoggingAudioHandler extends CompositeAudioHandler { } /// An [AudioHandler] for playing a list of podcast episodes. -class AudioPlayerHandler extends BaseAudioHandler +class AudioPlayerHandler extends BaseFlutterAudioHandler with QueueHandler, SeekHandler { // ignore: close_sinks final BehaviorSubject> _recentSubject = diff --git a/audio_service/lib/audio_service.dart b/audio_service/lib/audio_service.dart index b3ce5696..65249fe7 100644 --- a/audio_service/lib/audio_service.dart +++ b/audio_service/lib/audio_service.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:isolate'; import 'dart:ui'; +import 'package:audio_service_dart/audio_service_dart.dart'; +import 'package:audio_service_dart/base_audio_handler.dart'; import 'package:audio_service_platform_interface/audio_service_platform_interface.dart'; import 'package:audio_session/audio_session.dart'; import 'package:flutter/foundation.dart'; @@ -9,603 +11,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:rxdart/rxdart.dart'; -AudioServicePlatform _platform = AudioServicePlatform.instance; - -/// The different buttons on a headset. -enum MediaButton { - media, - next, - previous, -} - -/// The actons associated with playing audio. -enum MediaAction { - stop, - pause, - play, - rewind, - skipToPrevious, - skipToNext, - fastForward, - setRating, - seek, - playPause, - playFromMediaId, - playFromSearch, - skipToQueueItem, - playFromUri, - prepare, - prepareFromMediaId, - prepareFromSearch, - prepareFromUri, - setRepeatMode, - unused_1, - unused_2, - setShuffleMode, - seekBackward, - seekForward, -} - -/// The different states during audio processing. -enum AudioProcessingState { - idle, - loading, - buffering, - ready, - completed, - 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. [BasicPlaybackState.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 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? 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. - /// [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 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), - this.updateTime = updateTime ?? DateTime.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 [this.position]. [errorCode] and - /// [errorMessage] will be set to null unless [processingState] is - /// [AudioProcessingState.error]. - PlaybackState copyWith({ - AudioProcessingState? processingState, - bool? playing, - List? controls, - List? androidCompactActionIndices, - Set? systemActions, - Duration? updatePosition, - Duration? bufferedPosition, - double? speed, - int? errorCode, - String? errorMessage, - AudioServiceRepeatMode? repeatMode, - AudioServiceShuffleMode? shuffleMode, - bool? captioningEnabled, - int? queueIndex, - }) { - processingState ??= this.processingState; - return PlaybackState( - processingState: processingState, - playing: playing ?? this.playing, - controls: controls ?? this.controls, - androidCompactActionIndices: - androidCompactActionIndices ?? this.androidCompactActionIndices, - systemActions: systemActions ?? this.systemActions, - updatePosition: updatePosition ?? this.position, - bufferedPosition: bufferedPosition ?? this.bufferedPosition, - speed: speed ?? this.speed, - errorCode: processingState != AudioProcessingState.error - ? null - : (errorCode ?? this.errorCode), - errorMessage: processingState != AudioProcessingState.error - ? null - : (errorMessage ?? this.errorMessage), - repeatMode: repeatMode ?? this.repeatMode, - shuffleMode: shuffleMode ?? this.shuffleMode, - captioningEnabled: captioningEnabled ?? this.captioningEnabled, - queueIndex: queueIndex ?? this.queueIndex, - ); - } - - /// The current playback position. - Duration get position { - if (playing && processingState == AudioProcessingState.ready) { - return Duration( - milliseconds: (updatePosition.inMilliseconds + - ((DateTime.now().millisecondsSinceEpoch - - updateTime.millisecondsSinceEpoch) * - speed)) - .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()}'; -} - -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 dynamic _value; - - const Rating._internal(this._type, this._value); - - /// Creates a new heart rating. - const Rating.newHeartRating(bool hasHeart) - : this._internal(RatingStyle.heart, hasHeart); - - /// Creates a new percentage rating. - factory Rating.newPercentageRating(double percent) { - if (percent < 0 || percent > 100) throw ArgumentError(); - return Rating._internal(RatingStyle.percentage, percent); - } - - /// Creates a new star rating. - factory Rating.newStarRating(RatingStyle starRatingStyle, int starRating) { - if (starRatingStyle != RatingStyle.range3stars && - starRatingStyle != RatingStyle.range4stars && - starRatingStyle != RatingStyle.range5stars) { - throw ArgumentError(); - } - if (starRating > starRatingStyle.index || starRating < 0) - throw ArgumentError(); - return Rating._internal(starRatingStyle, starRating); - } - - /// Creates a new thumb rating. - const Rating.newThumbRating(bool isThumbsUp) - : this._internal(RatingStyle.thumbUpDown, isThumbsUp); - - /// Creates a new unrated rating. - const Rating.newUnratedRating(RatingStyle ratingStyle) - : this._internal(ratingStyle, null); - - /// Return the rating style. - RatingStyle getRatingStyle() => _type; - - /// Returns a percentage rating value greater or equal to 0.0f, 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; - if (_value < 0 || _value > 100) return -1; - return _value ?? -1; - } - - /// Returns a rating value greater or equal to 0.0f, 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 ?? -1; - } - - /// Returns true if the rating is "heart selected" or false if the - /// rating is "heart unselected", if the rating style is not [heart] - /// or if it is unrated. - bool hasHeart() { - if (_type != RatingStyle.heart) return false; - return _value ?? false; - } - - /// Returns true if the rating is "thumb up" or false if the rating - /// is "thumb down", if the rating style is not [thumbUpDown] or if - /// it is unrated. - bool isThumbUp() { - if (_type != RatingStyle.thumbUpDown) return false; - return _value ?? 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()}'; -} - -/// Metadata about an audio item that can be played, or a folder containing -/// audio items. -class MediaItem { - /// A unique id. - final String id; - - /// The album this media item belongs to. - final String album; - - /// The title of this media item. - final String title; - - /// 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 for this media item as a uri. - final Uri? artUri; - - /// 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 MediaItem. - final Rating? rating; - - /// A map of additional metadata for the media item. - /// - /// The values must be integers or strings. - final Map? extras; - - /// Creates a [MediaItem]. - /// - /// [id], [album] and [title] must not be null, and [id] must be unique for - /// each instance. - const MediaItem({ - required this.id, - required this.album, - required this.title, - this.artist, - this.genre, - this.duration, - this.artUri, - this.playable = true, - this.displayTitle, - this.displaySubtitle, - this.displayDescription, - this.rating, - this.extras, - }); - - /// Creates a copy of this [MediaItem] with with the given fields replaced by - /// new values. - MediaItem copyWith({ - String? id, - String? album, - String? title, - String? artist, - String? genre, - Duration? duration, - Uri? artUri, - bool? playable, - String? displayTitle, - String? displaySubtitle, - String? displayDescription, - Rating? rating, - Map? extras, - }) => - MediaItem( - id: id ?? this.id, - album: album ?? this.album, - title: title ?? this.title, - artist: artist ?? this.artist, - genre: genre ?? this.genre, - duration: duration ?? this.duration, - artUri: artUri ?? this.artUri, - playable: playable ?? this.playable, - displayTitle: displayTitle ?? this.displayTitle, - displaySubtitle: displaySubtitle ?? this.displaySubtitle, - displayDescription: displayDescription ?? this.displayDescription, - rating: rating ?? this.rating, - extras: extras ?? this.extras, - ); - - @override - int get hashCode => id.hashCode; - - @override - bool operator ==(dynamic other) => 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(), - extras: extras, - ); - - @override - String toString() => '${_toMessage().toMap()}'; -} - -/// 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 final stop = MediaControl( - androidIcon: 'drawable/audio_service_stop', - label: 'Stop', - action: MediaAction.stop, - ); - - /// A default control for [MediaAction.pause]. - static final pause = MediaControl( - androidIcon: 'drawable/audio_service_pause', - label: 'Pause', - action: MediaAction.pause, - ); - - /// A default control for [MediaAction.play]. - static final play = MediaControl( - androidIcon: 'drawable/audio_service_play_arrow', - label: 'Play', - action: MediaAction.play, - ); - - /// A default control for [MediaAction.rewind]. - static final rewind = MediaControl( - androidIcon: 'drawable/audio_service_fast_rewind', - label: 'Rewind', - action: MediaAction.rewind, - ); - - /// A default control for [MediaAction.skipToNext]. - static final skipToNext = MediaControl( - androidIcon: 'drawable/audio_service_skip_next', - label: 'Next', - action: MediaAction.skipToNext, - ); - - /// A default control for [MediaAction.skipToPrevious]. - static final skipToPrevious = MediaControl( - androidIcon: 'drawable/audio_service_skip_previous', - label: 'Previous', - action: MediaAction.skipToPrevious, - ); - - /// A default control for [MediaAction.fastForward]. - static final 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; - - const MediaControl({ - required this.androidIcon, - required this.label, - required this.action, - }); - - MediaControlMessage _toMessage() => MediaControlMessage( - androidIcon: androidIcon, - label: label, - action: MediaActionMessage.values[action.index], - ); +export 'package:audio_service_dart/audio_service_dart.dart'; - @override - String toString() => '${_toMessage().toMap()}'; -} +AudioServicePlatform _platform = AudioServicePlatform.instance; /// Provides an API to manage the app's [AudioHandler]. An app must call [init] /// during initialisation to register the [AudioHandler] that will service all @@ -879,7 +287,7 @@ class AudioService { // We haven't fetched the art yet, so show the metadata now, and again // after we load the art. await _platform.setMediaItem( - SetMediaItemRequest(mediaItem: mediaItem._toMessage())); + SetMediaItemRequest(mediaItem: mediaItem.toMessage())); // Load the art filePath = await _loadArtwork(mediaItem); // If we failed to download the art, abort. @@ -894,16 +302,16 @@ class AudioService { final platformMediaItem = mediaItem.copyWith(extras: extras); // Show the media item after the art is loaded. await _platform.setMediaItem( - SetMediaItemRequest(mediaItem: platformMediaItem._toMessage())); + SetMediaItemRequest(mediaItem: platformMediaItem.toMessage())); } else { await _platform.setMediaItem( - SetMediaItemRequest(mediaItem: mediaItem._toMessage())); + SetMediaItemRequest(mediaItem: mediaItem.toMessage())); } }); _handler.androidPlaybackInfo .listen((AndroidPlaybackInfo playbackInfo) async { await _platform.setAndroidPlaybackInfo(SetAndroidPlaybackInfoRequest( - playbackInfo: playbackInfo._toMessage())); + playbackInfo: playbackInfo.toMessage())); }); _handler.queue.listen((List? queue) async { if (queue == null) return; @@ -911,11 +319,11 @@ class AudioService { _loadAllArtwork(queue); } await _platform.setQueue(SetQueueRequest( - queue: queue.map((item) => item._toMessage()).toList())); + queue: queue.map((item) => item.toMessage()).toList())); }); _handler.playbackState.listen((PlaybackState playbackState) async { await _platform - .setState(SetStateRequest(state: playbackState._toMessage())); + .setState(SetStateRequest(state: playbackState.toMessage())); }); return handler; @@ -1490,583 +898,113 @@ abstract class BackgroundAudioTask extends BaseAudioHandler { onLoadChildren(parentMediaId); } -/// 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 prepare(); - - /// Prepare a specific media item for playback. - Future prepareFromMediaId(String mediaId, - [Map? extras]); - - /// Prepare playback from a search query. - Future prepareFromSearch(String query, [Map? extras]); - - /// Prepare a media item represented by a Uri for playback. - Future prepareFromUri(Uri uri, [Map? extras]); - - /// Start or resume playback. - Future play(); - - /// Play a specific media item. - Future playFromMediaId(String mediaId, [Map? extras]); - - /// Begin playback from a search query. - Future playFromSearch(String query, [Map? extras]); - - /// Play a media item represented by a Uri. - Future playFromUri(Uri uri, [Map? extras]); - - /// Play a specific media item. - Future playMediaItem(MediaItem mediaItem); - - /// Pause playback. - Future pause(); - - /// Process a headset button click, where [button] defaults to - /// [MediaButton.media]. - Future click([MediaButton button = MediaButton.media]); - - /// Stop playback and release resources. - Future stop(); - - /// Add [mediaItem] to the queue. - Future addQueueItem(MediaItem mediaItem); - - /// Add [mediaItems] to the queue. - Future addQueueItems(List mediaItems); - - /// Insert [mediaItem] into the queue at position [index]. - Future insertQueueItem(int index, MediaItem mediaItem); - - /// Update to the queue to [queue]. - Future updateQueue(List queue); - - /// Update the properties of [mediaItem]. - Future updateMediaItem(MediaItem mediaItem); - - /// Remove [mediaItem] from the queue. - Future removeQueueItem(MediaItem mediaItem); - - /// Remove at media item from the queue at the specified [index]. - Future removeQueueItemAt(int index); - - /// Skip to the next item in the queue. - Future skipToNext(); - - /// Skip to the previous item in the queue. - Future skipToPrevious(); - - /// Jump forward by [AudioServiceConfig.fastForwardInterval]. - Future fastForward(); - - /// Jump backward by [AudioServiceConfig.rewindInterval]. Note: this value - /// must be positive. - Future rewind(); - - /// Skip to a queue item. - Future skipToQueueItem(int index); - - /// Seek to [position]. - Future seek(Duration position); - - /// Set the rating. - Future setRating(Rating rating, Map? extras); - - Future setCaptioningEnabled(bool enabled); - - /// Set the repeat mode. - Future setRepeatMode(AudioServiceRepeatMode repeatMode); - - /// Set the shuffle mode. - Future setShuffleMode(AudioServiceShuffleMode shuffleMode); - - /// Begin or end seeking backward continuously. - Future seekBackward(bool begin); - - /// Begin or end seeking forward continuously. - Future seekForward(bool begin); - - /// Set the playback speed. - Future setSpeed(double speed); - - /// A mechanism to support app-specific actions. - Future customAction(String name, Map? extras); - - /// Handle the task being swiped away in the task manager (Android). - Future onTaskRemoved(); - - /// Handle the notification being swiped away (Android). - Future onNotificationDeleted(); - - /// Get the children of a parent media item. - Future> getChildren(String parentMediaId, - [Map? 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?> subscribeToChildren(String parentMediaId); - - /// Get a particular media item. - Future getMediaItem(String mediaId); - - /// Search for media items. - Future> search(String query, [Map? extras]); - - /// Set the remote volume on Android. This works only when using - /// [RemoteAndroidPlaybackInfo]. - Future androidSetRemoteVolume(int volumeIndex); - - /// Adjust the remote volume on Android. This works only when - /// [AndroidPlaybackInfo.playbackType] is [AndroidPlaybackType.remote]. - Future androidAdjustRemoteVolume(AndroidVolumeDirection direction); - - /// A value stream of playback states. - ValueStream get playbackState; - - /// A value stream of the current queue. - ValueStream?> get queue; - - /// A value stream of the current queueTitle. - ValueStream get queueTitle; - - /// A value stream of the current media item. - ValueStream get mediaItem; - - /// A value stream of the current rating style. - ValueStream get ratingStyle; +class _IsolateRequest { + /// The send port for sending the response of this request. + final SendPort sendPort; + final String method; + final List? arguments; - /// A value stream of the current [AndroidPlaybackInfo]. - ValueStream get androidPlaybackInfo; + _IsolateRequest(this.sendPort, this.method, [this.arguments]); +} - /// A stream of custom events. - Stream get customEvent; +const _isolatePortName = 'com.ryanheise.audioservice.port'; - /// A stream of custom states. - ValueStream get customState; -} +class _IsolateAudioHandler extends AudioHandler { + final _childrenSubjects = ?>>{}; -/// A [SwitchAudioHandler] wraps another [AudioHandler] that may be switched for -/// another at any time by setting [inner]. -class SwitchAudioHandler extends CompositeAudioHandler { @override // ignore: close_sinks - final BehaviorSubject playbackState = BehaviorSubject(); + final BehaviorSubject playbackState = + BehaviorSubject.seeded(PlaybackState()); @override // ignore: close_sinks - final BehaviorSubject?> queue = BehaviorSubject(); + final BehaviorSubject?> queue = + BehaviorSubject.seeded([]); @override + // TODO // ignore: close_sinks - final BehaviorSubject queueTitle = BehaviorSubject(); + final BehaviorSubject queueTitle = BehaviorSubject.seeded(''); @override // ignore: close_sinks - final BehaviorSubject mediaItem = BehaviorSubject(); + final BehaviorSubject mediaItem = BehaviorSubject.seeded(null); @override + // TODO // ignore: close_sinks final BehaviorSubject androidPlaybackInfo = BehaviorSubject(); @override + // TODO // ignore: close_sinks final BehaviorSubject ratingStyle = BehaviorSubject(); @override + // TODO // ignore: close_sinks final PublishSubject customEvent = PublishSubject(); + @override + // TODO // ignore: close_sinks final BehaviorSubject customState = BehaviorSubject(); - StreamSubscription? playbackStateSubscription; - StreamSubscription?>? queueSubscription; - StreamSubscription? queueTitleSubscription; - StreamSubscription? mediaItemSubscription; - StreamSubscription? androidPlaybackInfoSubscription; - StreamSubscription? ratingStyleSubscription; - StreamSubscription? customEventSubscription; - StreamSubscription? customStateSubscription; - - 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); - 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); + _IsolateAudioHandler() { + _platform.setClientCallbacks(_ClientCallbacks(this)); } -} - -/// 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 prepare() => _inner.prepare(); + Future prepare() => _send('prepare'); @override - @mustCallSuper Future prepareFromMediaId(String mediaId, [Map? extras]) => - _inner.prepareFromMediaId(mediaId, extras); + _send('prepareFromMediaId', [mediaId, extras]); @override - @mustCallSuper Future prepareFromSearch(String query, [Map? extras]) => - _inner.prepareFromSearch(query, extras); + _send('prepareFromSearch', [query, extras]); @override - @mustCallSuper Future prepareFromUri(Uri uri, [Map? extras]) => - _inner.prepareFromUri(uri, extras); + _send('prepareFromUri', [uri, extras]); @override - @mustCallSuper - Future play() => _inner.play(); + Future play() => _send('play'); @override - @mustCallSuper Future playFromMediaId(String mediaId, [Map? extras]) => - _inner.playFromMediaId(mediaId, extras); + _send('playFromMediaId', [mediaId, extras]); @override - @mustCallSuper Future playFromSearch(String query, [Map? extras]) => - _inner.playFromSearch(query, extras); + _send('playFromSearch', [query, extras]); @override - @mustCallSuper Future playFromUri(Uri uri, [Map? extras]) => - _inner.playFromUri(uri, extras); + _send('playFromUri', [uri, extras]); @override - @mustCallSuper Future playMediaItem(MediaItem mediaItem) => - _inner.playMediaItem(mediaItem); + _send('playMediaItem', [mediaItem]); @override - @mustCallSuper - Future pause() => _inner.pause(); + Future pause() => _send('pause'); @override - @mustCallSuper Future click([MediaButton button = MediaButton.media]) => - _inner.click(button); + _send('click', [button]); @override @mustCallSuper - Future stop() => _inner.stop(); + Future stop() => _send('stop'); @override - @mustCallSuper Future addQueueItem(MediaItem mediaItem) => - _inner.addQueueItem(mediaItem); + _send('addQueueItem', [mediaItem]); @override - @mustCallSuper Future addQueueItems(List mediaItems) => - _inner.addQueueItems(mediaItems); - - @override - @mustCallSuper - Future insertQueueItem(int index, MediaItem mediaItem) => - _inner.insertQueueItem(index, mediaItem); - - @override - @mustCallSuper - Future updateQueue(List queue) => _inner.updateQueue(queue); - - @override - @mustCallSuper - Future updateMediaItem(MediaItem mediaItem) => - _inner.updateMediaItem(mediaItem); - - @override - @mustCallSuper - Future removeQueueItem(MediaItem mediaItem) => - _inner.removeQueueItem(mediaItem); - - @override - @mustCallSuper - Future removeQueueItemAt(int index) => _inner.removeQueueItemAt(index); - - @override - @mustCallSuper - Future skipToNext() => _inner.skipToNext(); - - @override - @mustCallSuper - Future skipToPrevious() => _inner.skipToPrevious(); - - @override - @mustCallSuper - Future fastForward() => _inner.fastForward(); - - @override - @mustCallSuper - Future rewind() => _inner.rewind(); - - @override - @mustCallSuper - Future skipToQueueItem(int index) => _inner.skipToQueueItem(index); - - @override - @mustCallSuper - Future seek(Duration position) => _inner.seek(position); - - @override - @mustCallSuper - Future setRating(Rating rating, Map? extras) => - _inner.setRating(rating, extras); - - @override - @mustCallSuper - Future setCaptioningEnabled(bool enabled) => - _inner.setCaptioningEnabled(enabled); - - @override - @mustCallSuper - Future setRepeatMode(AudioServiceRepeatMode repeatMode) => - _inner.setRepeatMode(repeatMode); - - @override - @mustCallSuper - Future setShuffleMode(AudioServiceShuffleMode shuffleMode) => - _inner.setShuffleMode(shuffleMode); - - @override - @mustCallSuper - Future seekBackward(bool begin) => _inner.seekBackward(begin); - - @override - @mustCallSuper - Future seekForward(bool begin) => _inner.seekForward(begin); - - @override - @mustCallSuper - Future setSpeed(double speed) => _inner.setSpeed(speed); - - @override - @mustCallSuper - Future customAction(String name, Map? extras) => - _inner.customAction(name, extras); - - @override - @mustCallSuper - Future onTaskRemoved() => _inner.onTaskRemoved(); - - @override - @mustCallSuper - Future onNotificationDeleted() => _inner.onNotificationDeleted(); - - @override - @mustCallSuper - Future> getChildren(String parentMediaId, - [Map? options]) => - _inner.getChildren(parentMediaId, options); - - @override - @mustCallSuper - ValueStream?> subscribeToChildren( - String parentMediaId) => - _inner.subscribeToChildren(parentMediaId); - - @override - @mustCallSuper - Future getMediaItem(String mediaId) => - _inner.getMediaItem(mediaId); - - @override - @mustCallSuper - Future> search(String query, - [Map? extras]) => - _inner.search(query, extras); - - @override - @mustCallSuper - Future androidSetRemoteVolume(int volumeIndex) => - _inner.androidSetRemoteVolume(volumeIndex); - - @override - @mustCallSuper - Future androidAdjustRemoteVolume(AndroidVolumeDirection direction) => - _inner.androidAdjustRemoteVolume(direction); - - @override - ValueStream get playbackState => _inner.playbackState; - - @override - ValueStream?> get queue => _inner.queue; - - @override - ValueStream get queueTitle => _inner.queueTitle; - - @override - ValueStream get mediaItem => _inner.mediaItem; - - @override - ValueStream get ratingStyle => _inner.ratingStyle; - - @override - ValueStream get androidPlaybackInfo => - _inner.androidPlaybackInfo; - - @override - Stream get customEvent => _inner.customEvent; - - @override - ValueStream get customState => _inner.customState; -} - -class _IsolateRequest { - /// The send port for sending the response of this request. - final SendPort sendPort; - final String method; - final List? arguments; - - _IsolateRequest(this.sendPort, this.method, [this.arguments]); -} - -const _isolatePortName = 'com.ryanheise.audioservice.port'; - -class _IsolateAudioHandler extends AudioHandler { - final _childrenSubjects = ?>>{}; - - @override - // ignore: close_sinks - final BehaviorSubject playbackState = - BehaviorSubject.seeded(PlaybackState()); - @override - // ignore: close_sinks - final BehaviorSubject?> queue = - BehaviorSubject.seeded([]); - @override - // TODO - // ignore: close_sinks - final BehaviorSubject queueTitle = BehaviorSubject.seeded(''); - @override - // ignore: close_sinks - final BehaviorSubject mediaItem = BehaviorSubject.seeded(null); - @override - // TODO - // ignore: close_sinks - final BehaviorSubject androidPlaybackInfo = - BehaviorSubject(); - @override - // TODO - // ignore: close_sinks - final BehaviorSubject ratingStyle = BehaviorSubject(); - @override - // TODO - // ignore: close_sinks - final PublishSubject customEvent = PublishSubject(); - - @override - // TODO - // ignore: close_sinks - final BehaviorSubject customState = BehaviorSubject(); - - _IsolateAudioHandler() : super._() { - _platform.setClientCallbacks(_ClientCallbacks(this)); - } - - @override - Future prepare() => _send('prepare'); - - @override - Future prepareFromMediaId(String mediaId, - [Map? extras]) => - _send('prepareFromMediaId', [mediaId, extras]); - - @override - Future prepareFromSearch(String query, - [Map? extras]) => - _send('prepareFromSearch', [query, extras]); - - @override - Future prepareFromUri(Uri uri, [Map? extras]) => - _send('prepareFromUri', [uri, extras]); - - @override - Future play() => _send('play'); - - @override - Future playFromMediaId(String mediaId, - [Map? extras]) => - _send('playFromMediaId', [mediaId, extras]); - - @override - Future playFromSearch(String query, [Map? extras]) => - _send('playFromSearch', [query, extras]); - - @override - Future playFromUri(Uri uri, [Map? extras]) => - _send('playFromUri', [uri, extras]); - - @override - Future playMediaItem(MediaItem mediaItem) => - _send('playMediaItem', [mediaItem]); - - @override - Future pause() => _send('pause'); - - @override - Future click([MediaButton button = MediaButton.media]) => - _send('click', [button]); - - @override - @mustCallSuper - Future stop() => _send('stop'); - - @override - Future addQueueItem(MediaItem mediaItem) => - _send('addQueueItem', [mediaItem]); - - @override - Future addQueueItems(List mediaItems) => - _send('addQueueItems', [mediaItems]); + _send('addQueueItems', [mediaItems]); @override Future insertQueueItem(int index, MediaItem mediaItem) => @@ -2189,25 +1127,8 @@ class _IsolateAudioHandler extends AudioHandler { } } -/// Base class for implementations of [AudioHandler]. It provides default -/// implementations of all methods and provides controllers for emitting stream -/// events: -/// -/// * [playbackStateSubject] is a [BehaviorSubject] that emits events to -/// [playbackStateStream]. -/// * [queueSubject] is a [BehaviorSubject] that emits events to [queueStream]. -/// * [mediaItemSubject] is a [BehaviorSubject] that emits events to -/// [mediaItemStream]. -/// * [customEventSubject] is a [PublishSubject] that emits events to -/// [customEvent]. -/// -/// 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. +/// An implementation of [BaseAudioHandler] for Flutter that integrates with +/// the platform. /// /// ## Android service lifecycle and state transitions /// @@ -2240,426 +1161,18 @@ class _IsolateAudioHandler extends AudioHandler { /// 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.copyWith(playing: true)); - /// ``` - @override - // ignore: close_sinks - final BehaviorSubject 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 + [additionalItem]); - /// ``` - @override - final BehaviorSubject?> queue = - BehaviorSubject.seeded([]); - - /// 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 - // ignore: close_sinks - final BehaviorSubject 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 - // ignore: close_sinks - final BehaviorSubject 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 - // ignore: close_sinks - final BehaviorSubject 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(item); - /// ``` - @override - // ignore: close_sinks - final BehaviorSubject ratingStyle = BehaviorSubject(); - - /// A controller for broadcasting a custom event to the app's UI. Example - /// usage: - /// - /// ```dart - /// customEventSubject.add(MyCustomEvent(arg: 3)); - /// ``` - @protected - // ignore: close_sinks - final customEventSubject = PublishSubject(); - - /// A controller for broadcasting the current custom state to the app's UI. - /// Example usage: - /// - /// ```dart - /// customState.add(MyCustomState(...)); - /// ``` - @override - // ignore: close_sinks - final BehaviorSubject customState = BehaviorSubject(); - - BaseAudioHandler() : super._(); - - @override - Future prepare() async {} - - @override - Future prepareFromMediaId(String mediaId, - [Map? extras]) async {} - - @override - Future prepareFromSearch(String query, - [Map? extras]) async {} - - @override - Future prepareFromUri(Uri uri, [Map? extras]) async {} - - @override - Future play() async {} - - @override - Future playFromMediaId(String mediaId, - [Map? extras]) async {} - - @override - Future playFromSearch(String query, - [Map? extras]) async {} - - @override - Future playFromUri(Uri uri, [Map? extras]) async {} - - @override - Future playMediaItem(MediaItem mediaItem) async {} - - @override - Future pause() async {} +class BaseFlutterAudioHandler extends BaseAudioHandler { + /// Provides [SeekHandler.fastForwardInterval] when using [SeekHandler]. + Duration get fastForwardInterval => AudioService.config.fastForwardInterval; - @override - Future click([MediaButton button = MediaButton.media]) async { - switch (button) { - case MediaButton.media: - if (playbackState.value?.playing == true) { - await pause(); - } else { - await play(); - } - break; - case MediaButton.next: - await skipToNext(); - break; - case MediaButton.previous: - await skipToPrevious(); - break; - } - } + /// Provides [SeekHandler.rewindInterval] when using [SeekHandler]. + Duration get rewindInterval => AudioService.config.rewindInterval; @override @mustCallSuper Future stop() async { await AudioService._stop(); } - - @override - Future addQueueItem(MediaItem mediaItem) async {} - - @override - Future addQueueItems(List mediaItems) async {} - - @override - Future insertQueueItem(int index, MediaItem mediaItem) async {} - - @override - Future updateQueue(List queue) async {} - - @override - Future updateMediaItem(MediaItem mediaItem) async {} - - @override - Future removeQueueItem(MediaItem mediaItem) async {} - - @override - Future removeQueueItemAt(int index) async {} - - @override - Future skipToNext() async {} - - @override - Future skipToPrevious() async {} - - @override - Future fastForward() async {} - - @override - Future rewind() async {} - - @override - Future skipToQueueItem(int index) async {} - - @override - Future seek(Duration position) async {} - - @override - Future setRating(Rating rating, Map? extras) async {} - - @override - Future setCaptioningEnabled(bool enabled) async {} - - @override - Future setRepeatMode(AudioServiceRepeatMode repeatMode) async {} - - @override - Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async {} - - @override - Future seekBackward(bool begin) async {} - - @override - Future seekForward(bool begin) async {} - - @override - Future setSpeed(double speed) async {} - - @override - Future customAction( - String name, Map? arguments) async {} - - @override - Future onTaskRemoved() async {} - - @override - Future onNotificationDeleted() async { - await stop(); - } - - @override - Future> getChildren(String parentMediaId, - [Map? options]) async => - []; - - @override - ValueStream> subscribeToChildren(String parentMediaId) => - BehaviorSubject.seeded({}); - - @override - Future getMediaItem(String mediaId) async => null; - - @override - Future> search(String query, - [Map? extras]) async => - []; - - @override - Future androidAdjustRemoteVolume( - AndroidVolumeDirection direction) async {} - - @override - Future androidSetRemoteVolume(int volumeIndex) async {} - - @override - Stream get customEvent => customEventSubject.stream; -} - -/// 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 fastForward() => - _seekRelative(AudioService.config.fastForwardInterval); - - @override - Future rewind() => _seekRelative(-AudioService.config.rewindInterval); - - @override - Future seekForward(bool begin) async => _seekContinuously(begin, 1); - - @override - Future seekBackward(bool begin) async => _seekContinuously(begin, -1); - - /// Jumps away from the current position by [offset]. - Future _seekRelative(Duration offset) async { - var newPosition = playbackState.value!.position + offset; - // Make sure we don't jump out of bounds. - if (newPosition < Duration.zero) { - newPosition = Duration.zero; - } - final duration = mediaItem.value?.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.value?.duration != null) { - _seeker = _Seeker(this, Duration(seconds: 10 * direction), - Duration(seconds: 1), mediaItem.value!.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, - ); - - start() async { - _running = true; - while (_running) { - Duration newPosition = - handler.playbackState.value!.position + positionInterval; - if (newPosition < Duration.zero) newPosition = Duration.zero; - if (newPosition > duration) newPosition = duration; - handler.seek(newPosition); - await Future.delayed(stepInterval); - } - } - - 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 addQueueItem(MediaItem mediaItem) async { - queue.add(queue.value!..add(mediaItem)); - await super.addQueueItem(mediaItem); - } - - @override - Future addQueueItems(List mediaItems) async { - queue.add(queue.value!..addAll(mediaItems)); - await super.addQueueItems(mediaItems); - } - - @override - Future insertQueueItem(int index, MediaItem mediaItem) async { - queue.add(queue.value!..insert(index, mediaItem)); - await super.insertQueueItem(index, mediaItem); - } - - @override - Future updateQueue(List queue) async { - this.queue.add( - this.queue.value!..replaceRange(0, this.queue.value!.length, queue)); - await super.updateQueue(queue); - } - - @override - Future updateMediaItem(MediaItem mediaItem) async { - this.queue.add( - this.queue.value!..[this.queue.value!.indexOf(mediaItem)] = mediaItem); - await super.updateMediaItem(mediaItem); - } - - @override - Future removeQueueItem(MediaItem mediaItem) async { - queue.add(this.queue.value!..remove(mediaItem)); - await super.removeQueueItem(mediaItem); - } - - @override - Future skipToNext() async { - await _skip(1); - await super.skipToNext(); - } - - @override - Future skipToPrevious() async { - await _skip(-1); - await super.skipToPrevious(); - } - - /// This should be overridden to instruct how to skip to the queue item at - /// [index]. By default, this will broadcast [index] as - /// [PlaybackState.queueIndex] via the [playbackState] stream, and will - /// broadcast [queue] element [index] via the stream [mediaItem]. Your - /// implementation may call super to reuse this default implementation, or - /// else provide equivalent behaviour. - @override - Future skipToQueueItem(int index) async { - playbackState.add(playbackState.value!.copyWith(queueIndex: index)); - mediaItem.add(queue.value![index]); - await super.skipToQueueItem(index); - } - - Future _skip(int offset) async { - final queue = this.queue.value!; - final index = playbackState.value!.queueIndex!; - if (index < 0 || index >= queue.length) return; - await skipToQueueItem(index + offset); - } -} - -/// The available shuffle modes for the queue. -enum AudioServiceShuffleMode { none, all, 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] 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 registering an [AudioHandler]. @@ -2801,12 +1314,6 @@ class AndroidContentStyle { static final categoryGridItemHintValue = 4; } -/// (Maybe) temporary. -extension AudioServiceValueStream on ValueStream { - @Deprecated('Use "this" instead. Will be removed before the release') - ValueStream get stream => this; -} - extension MediaItemMessageExtension on MediaItemMessage { MediaItem toPlugin() => MediaItem( id: id, @@ -2826,7 +1333,7 @@ extension MediaItemMessageExtension on MediaItemMessage { } extension RatingMessageExtension on RatingMessage { - Rating toPlugin() => Rating._internal(RatingStyle.values[type.index], value); + Rating toPlugin() => Rating.fromRaw(RatingStyle.values[type.index], value); } extension AndroidVolumeDirectionMessageExtension @@ -2838,70 +1345,6 @@ extension MediaButtonMessageExtension on MediaButtonMessage { MediaButton toPlugin() => MediaButton.values[index]; } -class AndroidVolumeDirection { - static final lower = AndroidVolumeDirection(-1); - static final same = AndroidVolumeDirection(0); - static final raise = AndroidVolumeDirection(1); - static final values = { - -1: lower, - 0: same, - 1: raise, - }; - final int index; - - AndroidVolumeDirection(this.index); - - @override - String toString() => '$index'; -} - -enum AndroidVolumeControlType { fixed, relative, absolute } - -abstract class AndroidPlaybackInfo { - AndroidPlaybackInfoMessage _toMessage(); - - @override - String toString() => '${_toMessage().toMap()}'; -} - -class RemoteAndroidPlaybackInfo extends AndroidPlaybackInfo { - //final AndroidAudioAttributes audioAttributes; - final AndroidVolumeControlType volumeControlType; - final int maxVolume; - final int volume; - - RemoteAndroidPlaybackInfo({ - required this.volumeControlType, - required this.maxVolume, - required this.volume, - }); - - AndroidPlaybackInfo copyWith({ - AndroidVolumeControlType? volumeControlType, - int? maxVolume, - int? volume, - }) => - RemoteAndroidPlaybackInfo( - volumeControlType: volumeControlType ?? this.volumeControlType, - maxVolume: maxVolume ?? this.maxVolume, - volume: volume ?? this.volume, - ); - - @override - RemoteAndroidPlaybackInfoMessage _toMessage() => - RemoteAndroidPlaybackInfoMessage( - volumeControlType: - AndroidVolumeControlTypeMessage.values[volumeControlType.index], - maxVolume: maxVolume, - volume: volume, - ); -} - -class LocalAndroidPlaybackInfo extends AndroidPlaybackInfo { - LocalAndroidPlaybackInfoMessage _toMessage() => - LocalAndroidPlaybackInfoMessage(); -} - @deprecated class AudioServiceBackground { static BaseAudioHandler get _handler => @@ -3092,13 +1535,13 @@ class _HandlerCallbacks extends AudioHandlerCallbacks { final mediaItems = await _onLoadChildren(request.parentMediaId, request.options); return GetChildrenResponse( - children: mediaItems.map((item) => item._toMessage()).toList()); + children: mediaItems.map((item) => item.toMessage()).toList()); } @override Future getMediaItem(GetMediaItemRequest request) async { return GetMediaItemResponse( - mediaItem: (await handler.getMediaItem(request.mediaId))?._toMessage()); + mediaItem: (await handler.getMediaItem(request.mediaId))?.toMessage()); } @override @@ -3170,7 +1613,7 @@ class _HandlerCallbacks extends AudioHandlerCallbacks { @override Future search(SearchRequest request) async => SearchResponse( mediaItems: (await handler.search(request.query, request.extras)) - .map((item) => item._toMessage()) + .map((item) => item.toMessage()) .toList()); @override @@ -3229,6 +1672,7 @@ class _HandlerCallbacks extends AudioHandlerCallbacks { final Map?>> _childrenSubscriptions = >>{}; + Future> _onLoadChildren( String parentMediaId, Map? options) async { var childrenSubscription = _childrenSubscriptions[parentMediaId]; @@ -3282,9 +1726,9 @@ class _ClientCallbacks extends AudioClientCallbacks { handler.queue.add(request.queue.map((item) => item.toPlugin()).toList()); } - //@override - //Future onChildrenLoaded(OnChildrenLoadedRequest request) { - // // TODO: implement onChildrenLoaded - // throw UnimplementedError(); - //} +//@override +//Future onChildrenLoaded(OnChildrenLoadedRequest request) { +// // TODO: implement onChildrenLoaded +// throw UnimplementedError(); +//} } diff --git a/audio_service/pubspec.yaml b/audio_service/pubspec.yaml index 7448df6a..b8c28fb8 100644 --- a/audio_service/pubspec.yaml +++ b/audio_service/pubspec.yaml @@ -9,12 +9,19 @@ environment: dependencies: # Use these deps for local development + # audio_service_dart: + # path: ../audio_service_dart # audio_service_platform_interface: # path: ../audio_service_platform_interface # audio_service_web: # path: ../audio_service_web # Use these deps when pushing to origin (for the benefit of testers) + audio_service_dart: + git: + url: https://github.com/ryanheise/audio_service.git + ref: one-isolate + path: audio_service_dart audio_service_platform_interface: git: url: https://github.com/ryanheise/audio_service.git @@ -27,6 +34,7 @@ dependencies: path: audio_service_web # Use these deps when publishing. + # audio_service_dart: ^0.1.0 # audio_service_platform_interface: ^1.0.0 # audio_service_web: ^0.0.1 diff --git a/audio_service_dart/.gitignore b/audio_service_dart/.gitignore new file mode 100644 index 00000000..0c44ab06 --- /dev/null +++ b/audio_service_dart/.gitignore @@ -0,0 +1,13 @@ +# Files and directories created by pub +.dart_tool/ +.packages + +# Omit commiting pubspec.lock for library packages: +# https://dart.dev/guides/libraries/private-files#pubspeclock +pubspec.lock + +# Conventional directory for build outputs +build/ + +# Directory created by dartdoc +doc/api/ diff --git a/audio_service_dart/CHANGELOG.md b/audio_service_dart/CHANGELOG.md new file mode 100644 index 00000000..13187808 --- /dev/null +++ b/audio_service_dart/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release diff --git a/audio_service_dart/LICENSE b/audio_service_dart/LICENSE new file mode 100644 index 00000000..971b99d6 --- /dev/null +++ b/audio_service_dart/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ryan Heise and the project contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/audio_service_dart/lib/audio_service_dart.dart b/audio_service_dart/lib/audio_service_dart.dart new file mode 100644 index 00000000..111ed4a8 --- /dev/null +++ b/audio_service_dart/lib/audio_service_dart.dart @@ -0,0 +1,3 @@ +library audio_service_dart; + +export 'src/audio_service_dart.dart' hide BaseAudioHandler; diff --git a/audio_service_dart/lib/base_audio_handler.dart b/audio_service_dart/lib/base_audio_handler.dart new file mode 100644 index 00000000..4227c160 --- /dev/null +++ b/audio_service_dart/lib/base_audio_handler.dart @@ -0,0 +1 @@ +export 'src/audio_service_dart.dart' show BaseAudioHandler; diff --git a/audio_service_dart/lib/src/audio_service_dart.dart b/audio_service_dart/lib/src/audio_service_dart.dart new file mode 100644 index 00000000..b9ee33b2 --- /dev/null +++ b/audio_service_dart/lib/src/audio_service_dart.dart @@ -0,0 +1,1591 @@ +import 'dart:async'; + +import 'package:audio_service_platform_interface_entities/audio_service_platform_interface_entities.dart'; +import 'package:meta/meta.dart'; +import 'package:rxdart/rxdart.dart'; + +/// The different buttons on a headset. +enum MediaButton { + media, + next, + previous, +} + +/// The actons associated with playing audio. +enum MediaAction { + stop, + pause, + play, + rewind, + skipToPrevious, + skipToNext, + fastForward, + setRating, + seek, + playPause, + playFromMediaId, + playFromSearch, + skipToQueueItem, + playFromUri, + prepare, + prepareFromMediaId, + prepareFromSearch, + prepareFromUri, + setRepeatMode, + unused_1, + unused_2, + setShuffleMode, + seekBackward, + seekForward, +} + +/// The different states during audio processing. +enum AudioProcessingState { + idle, + loading, + buffering, + ready, + completed, + 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. [BasicPlaybackState.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 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? 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. + /// [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 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), + this.updateTime = updateTime ?? DateTime.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 [this.position]. [errorCode] and + /// [errorMessage] will be set to null unless [processingState] is + /// [AudioProcessingState.error]. + PlaybackState copyWith({ + AudioProcessingState? processingState, + bool? playing, + List? controls, + List? androidCompactActionIndices, + Set? systemActions, + Duration? updatePosition, + Duration? bufferedPosition, + double? speed, + int? errorCode, + String? errorMessage, + AudioServiceRepeatMode? repeatMode, + AudioServiceShuffleMode? shuffleMode, + bool? captioningEnabled, + int? queueIndex, + }) { + processingState ??= this.processingState; + return PlaybackState( + processingState: processingState, + playing: playing ?? this.playing, + controls: controls ?? this.controls, + androidCompactActionIndices: + androidCompactActionIndices ?? this.androidCompactActionIndices, + systemActions: systemActions ?? this.systemActions, + updatePosition: updatePosition ?? this.position, + bufferedPosition: bufferedPosition ?? this.bufferedPosition, + speed: speed ?? this.speed, + errorCode: processingState != AudioProcessingState.error + ? null + : (errorCode ?? this.errorCode), + errorMessage: processingState != AudioProcessingState.error + ? null + : (errorMessage ?? this.errorMessage), + repeatMode: repeatMode ?? this.repeatMode, + shuffleMode: shuffleMode ?? this.shuffleMode, + captioningEnabled: captioningEnabled ?? this.captioningEnabled, + queueIndex: queueIndex ?? this.queueIndex, + ); + } + + /// The current playback position. + Duration get position { + if (playing && processingState == AudioProcessingState.ready) { + return Duration( + milliseconds: (updatePosition.inMilliseconds + + ((DateTime.now().millisecondsSinceEpoch - + updateTime.millisecondsSinceEpoch) * + speed)) + .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()}'; +} + +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 dynamic _value; + + const Rating._internal(this._type, this._value); + + /// Constructs a rating from raw data. + /// + /// This constructor is useful for serialization; for other uses, use a + /// different constructor. + const Rating.fromRaw(RatingStyle type, dynamic value) + : this._internal(type, value); + + /// Creates a new heart rating. + const Rating.newHeartRating(bool hasHeart) + : this._internal(RatingStyle.heart, hasHeart); + + /// Creates a new percentage rating. + factory Rating.newPercentageRating(double percent) { + if (percent < 0 || percent > 100) throw ArgumentError(); + return Rating._internal(RatingStyle.percentage, percent); + } + + /// Creates a new star rating. + factory Rating.newStarRating(RatingStyle starRatingStyle, int starRating) { + if (starRatingStyle != RatingStyle.range3stars && + starRatingStyle != RatingStyle.range4stars && + starRatingStyle != RatingStyle.range5stars) { + throw ArgumentError(); + } + if (starRating > starRatingStyle.index || starRating < 0) + throw ArgumentError(); + return Rating._internal(starRatingStyle, starRating); + } + + /// Creates a new thumb rating. + const Rating.newThumbRating(bool isThumbsUp) + : this._internal(RatingStyle.thumbUpDown, isThumbsUp); + + /// Creates a new unrated rating. + const Rating.newUnratedRating(RatingStyle ratingStyle) + : this._internal(ratingStyle, null); + + /// Return the rating style. + RatingStyle getRatingStyle() => _type; + + /// Returns a percentage rating value greater or equal to 0.0f, 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; + if (_value < 0 || _value > 100) return -1; + return _value ?? -1; + } + + /// Returns a rating value greater or equal to 0.0f, 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 ?? -1; + } + + /// Returns true if the rating is "heart selected" or false if the + /// rating is "heart unselected", if the rating style is not [heart] + /// or if it is unrated. + bool hasHeart() { + if (_type != RatingStyle.heart) return false; + return _value ?? false; + } + + /// Returns true if the rating is "thumb up" or false if the rating + /// is "thumb down", if the rating style is not [thumbUpDown] or if + /// it is unrated. + bool isThumbUp() { + if (_type != RatingStyle.thumbUpDown) return false; + return _value ?? 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()}'; +} + +/// Metadata about an audio item that can be played, or a folder containing +/// audio items. +class MediaItem { + /// A unique id. + final String id; + + /// The album this media item belongs to. + final String album; + + /// The title of this media item. + final String title; + + /// 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 for this media item as a uri. + final Uri? artUri; + + /// 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 MediaItem. + final Rating? rating; + + /// A map of additional metadata for the media item. + /// + /// The values must be integers or strings. + final Map? extras; + + /// Creates a [MediaItem]. + /// + /// [id], [album] and [title] must not be null, and [id] must be unique for + /// each instance. + const MediaItem({ + required this.id, + required this.album, + required this.title, + this.artist, + this.genre, + this.duration, + this.artUri, + this.playable = true, + this.displayTitle, + this.displaySubtitle, + this.displayDescription, + this.rating, + this.extras, + }); + + /// Creates a copy of this [MediaItem] with with the given fields replaced by + /// new values. + MediaItem copyWith({ + String? id, + String? album, + String? title, + String? artist, + String? genre, + Duration? duration, + Uri? artUri, + bool? playable, + String? displayTitle, + String? displaySubtitle, + String? displayDescription, + Rating? rating, + Map? extras, + }) => + MediaItem( + id: id ?? this.id, + album: album ?? this.album, + title: title ?? this.title, + artist: artist ?? this.artist, + genre: genre ?? this.genre, + duration: duration ?? this.duration, + artUri: artUri ?? this.artUri, + playable: playable ?? this.playable, + displayTitle: displayTitle ?? this.displayTitle, + displaySubtitle: displaySubtitle ?? this.displaySubtitle, + displayDescription: displayDescription ?? this.displayDescription, + rating: rating ?? this.rating, + extras: extras ?? this.extras, + ); + + @override + int get hashCode => id.hashCode; + + @override + bool operator ==(dynamic other) => 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(), + extras: extras, + ); + + @override + String toString() => '${toMessage().toMap()}'; +} + +/// 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 final stop = MediaControl( + androidIcon: 'drawable/audio_service_stop', + label: 'Stop', + action: MediaAction.stop, + ); + + /// A default control for [MediaAction.pause]. + static final pause = MediaControl( + androidIcon: 'drawable/audio_service_pause', + label: 'Pause', + action: MediaAction.pause, + ); + + /// A default control for [MediaAction.play]. + static final play = MediaControl( + androidIcon: 'drawable/audio_service_play_arrow', + label: 'Play', + action: MediaAction.play, + ); + + /// A default control for [MediaAction.rewind]. + static final rewind = MediaControl( + androidIcon: 'drawable/audio_service_fast_rewind', + label: 'Rewind', + action: MediaAction.rewind, + ); + + /// A default control for [MediaAction.skipToNext]. + static final skipToNext = MediaControl( + androidIcon: 'drawable/audio_service_skip_next', + label: 'Next', + action: MediaAction.skipToNext, + ); + + /// A default control for [MediaAction.skipToPrevious]. + static final skipToPrevious = MediaControl( + androidIcon: 'drawable/audio_service_skip_previous', + label: 'Previous', + action: MediaAction.skipToPrevious, + ); + + /// A default control for [MediaAction.fastForward]. + static final 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; + + const MediaControl({ + required this.androidIcon, + required this.label, + required this.action, + }); + + MediaControlMessage toMessage() => MediaControlMessage( + androidIcon: androidIcon, + label: label, + action: MediaActionMessage.values[action.index], + ); + + @override + String toString() => '${toMessage().toMap()}'; +} + +/// 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. +/// +/// Instead of subclassing this class, consider subclassing [BaseAudioHandler], +/// which provides high-level state APIs. +/// For Flutter, use `BaseFlutterAudioHandler` in `package:audio_service`. +abstract class AudioHandler { + /// Prepare media items for playback. + Future prepare(); + + /// Prepare a specific media item for playback. + Future prepareFromMediaId(String mediaId, + [Map? extras]); + + /// Prepare playback from a search query. + Future prepareFromSearch(String query, [Map? extras]); + + /// Prepare a media item represented by a Uri for playback. + Future prepareFromUri(Uri uri, [Map? extras]); + + /// Start or resume playback. + Future play(); + + /// Play a specific media item. + Future playFromMediaId(String mediaId, [Map? extras]); + + /// Begin playback from a search query. + Future playFromSearch(String query, [Map? extras]); + + /// Play a media item represented by a Uri. + Future playFromUri(Uri uri, [Map? extras]); + + /// Play a specific media item. + Future playMediaItem(MediaItem mediaItem); + + /// Pause playback. + Future pause(); + + /// Process a headset button click, where [button] defaults to + /// [MediaButton.media]. + Future click([MediaButton button = MediaButton.media]); + + /// Stop playback and release resources. + Future stop(); + + /// Add [mediaItem] to the queue. + Future addQueueItem(MediaItem mediaItem); + + /// Add [mediaItems] to the queue. + Future addQueueItems(List mediaItems); + + /// Insert [mediaItem] into the queue at position [index]. + Future insertQueueItem(int index, MediaItem mediaItem); + + /// Update to the queue to [queue]. + Future updateQueue(List queue); + + /// Update the properties of [mediaItem]. + Future updateMediaItem(MediaItem mediaItem); + + /// Remove [mediaItem] from the queue. + Future removeQueueItem(MediaItem mediaItem); + + /// Remove at media item from the queue at the specified [index]. + Future removeQueueItemAt(int index); + + /// Skip to the next item in the queue. + Future skipToNext(); + + /// Skip to the previous item in the queue. + Future skipToPrevious(); + + /// Jump forward by [AudioServiceConfig.fastForwardInterval]. + Future fastForward(); + + /// Jump backward by [AudioServiceConfig.rewindInterval]. Note: this value + /// must be positive. + Future rewind(); + + /// Skip to a queue item. + Future skipToQueueItem(int index); + + /// Seek to [position]. + Future seek(Duration position); + + /// Set the rating. + Future setRating(Rating rating, Map? extras); + + Future setCaptioningEnabled(bool enabled); + + /// Set the repeat mode. + Future setRepeatMode(AudioServiceRepeatMode repeatMode); + + /// Set the shuffle mode. + Future setShuffleMode(AudioServiceShuffleMode shuffleMode); + + /// Begin or end seeking backward continuously. + Future seekBackward(bool begin); + + /// Begin or end seeking forward continuously. + Future seekForward(bool begin); + + /// Set the playback speed. + Future setSpeed(double speed); + + /// A mechanism to support app-specific actions. + Future customAction(String name, Map? extras); + + /// Handle the task being swiped away in the task manager (Android). + Future onTaskRemoved(); + + /// Handle the notification being swiped away (Android). + Future onNotificationDeleted(); + + /// Get the children of a parent media item. + Future> getChildren(String parentMediaId, + [Map? 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?> subscribeToChildren(String parentMediaId); + + /// Get a particular media item. + Future getMediaItem(String mediaId); + + /// Search for media items. + Future> search(String query, [Map? extras]); + + /// Set the remote volume on Android. This works only when using + /// [RemoteAndroidPlaybackInfo]. + Future androidSetRemoteVolume(int volumeIndex); + + /// Adjust the remote volume on Android. This works only when + /// [AndroidPlaybackInfo.playbackType] is [AndroidPlaybackType.remote]. + Future androidAdjustRemoteVolume(AndroidVolumeDirection direction); + + /// A value stream of playback states. + ValueStream get playbackState; + + /// A value stream of the current queue. + ValueStream?> get queue; + + /// A value stream of the current queueTitle. + ValueStream get queueTitle; + + /// A value stream of the current media item. + ValueStream get mediaItem; + + /// A value stream of the current rating style. + ValueStream get ratingStyle; + + /// A value stream of the current [AndroidPlaybackInfo]. + ValueStream get androidPlaybackInfo; + + /// A stream of custom events. + Stream get customEvent; + + /// A stream of custom states. + ValueStream get customState; +} + +/// A [SwitchAudioHandler] wraps another [AudioHandler] that may be switched for +/// another at any time by setting [inner]. +class SwitchAudioHandler extends CompositeAudioHandler { + @override + // ignore: close_sinks + final BehaviorSubject playbackState = BehaviorSubject(); + @override + // ignore: close_sinks + final BehaviorSubject?> queue = BehaviorSubject(); + @override + // ignore: close_sinks + final BehaviorSubject queueTitle = BehaviorSubject(); + @override + // ignore: close_sinks + final BehaviorSubject mediaItem = BehaviorSubject(); + @override + // ignore: close_sinks + final BehaviorSubject androidPlaybackInfo = + BehaviorSubject(); + @override + // ignore: close_sinks + final BehaviorSubject ratingStyle = BehaviorSubject(); + @override + // ignore: close_sinks + final PublishSubject customEvent = PublishSubject(); + @override + // ignore: close_sinks + final BehaviorSubject customState = BehaviorSubject(); + + StreamSubscription? playbackStateSubscription; + StreamSubscription?>? queueSubscription; + StreamSubscription? queueTitleSubscription; + StreamSubscription? mediaItemSubscription; + StreamSubscription? androidPlaybackInfoSubscription; + StreamSubscription? ratingStyleSubscription; + StreamSubscription? customEventSubscription; + StreamSubscription? customStateSubscription; + + 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); + 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; + + @override + @mustCallSuper + Future prepare() => _inner.prepare(); + + @override + @mustCallSuper + Future prepareFromMediaId(String mediaId, + [Map? extras]) => + _inner.prepareFromMediaId(mediaId, extras); + + @override + @mustCallSuper + Future prepareFromSearch(String query, + [Map? extras]) => + _inner.prepareFromSearch(query, extras); + + @override + @mustCallSuper + Future prepareFromUri(Uri uri, [Map? extras]) => + _inner.prepareFromUri(uri, extras); + + @override + @mustCallSuper + Future play() => _inner.play(); + + @override + @mustCallSuper + Future playFromMediaId(String mediaId, + [Map? extras]) => + _inner.playFromMediaId(mediaId, extras); + + @override + @mustCallSuper + Future playFromSearch(String query, [Map? extras]) => + _inner.playFromSearch(query, extras); + + @override + @mustCallSuper + Future playFromUri(Uri uri, [Map? extras]) => + _inner.playFromUri(uri, extras); + + @override + @mustCallSuper + Future playMediaItem(MediaItem mediaItem) => + _inner.playMediaItem(mediaItem); + + @override + @mustCallSuper + Future pause() => _inner.pause(); + + @override + @mustCallSuper + Future click([MediaButton button = MediaButton.media]) => + _inner.click(button); + + @override + @mustCallSuper + Future stop() => _inner.stop(); + + @override + @mustCallSuper + Future addQueueItem(MediaItem mediaItem) => + _inner.addQueueItem(mediaItem); + + @override + @mustCallSuper + Future addQueueItems(List mediaItems) => + _inner.addQueueItems(mediaItems); + + @override + @mustCallSuper + Future insertQueueItem(int index, MediaItem mediaItem) => + _inner.insertQueueItem(index, mediaItem); + + @override + @mustCallSuper + Future updateQueue(List queue) => _inner.updateQueue(queue); + + @override + @mustCallSuper + Future updateMediaItem(MediaItem mediaItem) => + _inner.updateMediaItem(mediaItem); + + @override + @mustCallSuper + Future removeQueueItem(MediaItem mediaItem) => + _inner.removeQueueItem(mediaItem); + + @override + @mustCallSuper + Future removeQueueItemAt(int index) => _inner.removeQueueItemAt(index); + + @override + @mustCallSuper + Future skipToNext() => _inner.skipToNext(); + + @override + @mustCallSuper + Future skipToPrevious() => _inner.skipToPrevious(); + + @override + @mustCallSuper + Future fastForward() => _inner.fastForward(); + + @override + @mustCallSuper + Future rewind() => _inner.rewind(); + + @override + @mustCallSuper + Future skipToQueueItem(int index) => _inner.skipToQueueItem(index); + + @override + @mustCallSuper + Future seek(Duration position) => _inner.seek(position); + + @override + @mustCallSuper + Future setRating(Rating rating, Map? extras) => + _inner.setRating(rating, extras); + + @override + @mustCallSuper + Future setCaptioningEnabled(bool enabled) => + _inner.setCaptioningEnabled(enabled); + + @override + @mustCallSuper + Future setRepeatMode(AudioServiceRepeatMode repeatMode) => + _inner.setRepeatMode(repeatMode); + + @override + @mustCallSuper + Future setShuffleMode(AudioServiceShuffleMode shuffleMode) => + _inner.setShuffleMode(shuffleMode); + + @override + @mustCallSuper + Future seekBackward(bool begin) => _inner.seekBackward(begin); + + @override + @mustCallSuper + Future seekForward(bool begin) => _inner.seekForward(begin); + + @override + @mustCallSuper + Future setSpeed(double speed) => _inner.setSpeed(speed); + + @override + @mustCallSuper + Future customAction(String name, Map? extras) => + _inner.customAction(name, extras); + + @override + @mustCallSuper + Future onTaskRemoved() => _inner.onTaskRemoved(); + + @override + @mustCallSuper + Future onNotificationDeleted() => _inner.onNotificationDeleted(); + + @override + @mustCallSuper + Future> getChildren(String parentMediaId, + [Map? options]) => + _inner.getChildren(parentMediaId, options); + + @override + @mustCallSuper + ValueStream?> subscribeToChildren( + String parentMediaId) => + _inner.subscribeToChildren(parentMediaId); + + @override + @mustCallSuper + Future getMediaItem(String mediaId) => + _inner.getMediaItem(mediaId); + + @override + @mustCallSuper + Future> search(String query, + [Map? extras]) => + _inner.search(query, extras); + + @override + @mustCallSuper + Future androidSetRemoteVolume(int volumeIndex) => + _inner.androidSetRemoteVolume(volumeIndex); + + @override + @mustCallSuper + Future androidAdjustRemoteVolume(AndroidVolumeDirection direction) => + _inner.androidAdjustRemoteVolume(direction); + + @override + ValueStream get playbackState => _inner.playbackState; + + @override + ValueStream?> get queue => _inner.queue; + + @override + ValueStream get queueTitle => _inner.queueTitle; + + @override + ValueStream get mediaItem => _inner.mediaItem; + + @override + ValueStream get ratingStyle => _inner.ratingStyle; + + @override + ValueStream get androidPlaybackInfo => + _inner.androidPlaybackInfo; + + @override + Stream get customEvent => _inner.customEvent; + + @override + ValueStream get customState => _inner.customState; +} + +/// Base class for implementations of [AudioHandler]. It provides default +/// implementations of all methods and provides controllers for emitting stream +/// events: +/// +/// * [playbackStateSubject] is a [BehaviorSubject] that emits events to +/// [playbackStateStream]. +/// * [queueSubject] is a [BehaviorSubject] that emits events to [queueStream]. +/// * [mediaItemSubject] is a [BehaviorSubject] that emits events to +/// [mediaItemStream]. +/// * [customEventSubject] is a [PublishSubject] that emits events to +/// [customEvent]. +/// +/// 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. +/// +/// ## Flutter +/// To properly integrate with Flutter platforms, use `BaseFlutterAudioHandler` +/// in `package:audio_service`. +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.copyWith(playing: true)); + /// ``` + @override + // ignore: close_sinks + final BehaviorSubject 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 + [additionalItem]); + /// ``` + @override + final BehaviorSubject?> queue = + BehaviorSubject.seeded([]); + + /// 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 + // ignore: close_sinks + final BehaviorSubject 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 + // ignore: close_sinks + final BehaviorSubject 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 + // ignore: close_sinks + final BehaviorSubject 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(item); + /// ``` + @override + // ignore: close_sinks + final BehaviorSubject ratingStyle = BehaviorSubject(); + + /// A controller for broadcasting a custom event to the app's UI. Example + /// usage: + /// + /// ```dart + /// customEventSubject.add(MyCustomEvent(arg: 3)); + /// ``` + @protected + // ignore: close_sinks + final customEventSubject = PublishSubject(); + + /// A controller for broadcasting the current custom state to the app's UI. + /// Example usage: + /// + /// ```dart + /// customState.add(MyCustomState(...)); + /// ``` + @override + // ignore: close_sinks + final BehaviorSubject customState = BehaviorSubject(); + + @override + Future prepare() async {} + + @override + Future prepareFromMediaId(String mediaId, + [Map? extras]) async {} + + @override + Future prepareFromSearch(String query, + [Map? extras]) async {} + + @override + Future prepareFromUri(Uri uri, [Map? extras]) async {} + + @override + Future play() async {} + + @override + Future playFromMediaId(String mediaId, + [Map? extras]) async {} + + @override + Future playFromSearch(String query, + [Map? extras]) async {} + + @override + Future playFromUri(Uri uri, [Map? extras]) async {} + + @override + Future playMediaItem(MediaItem mediaItem) async {} + + @override + Future pause() async {} + + @override + Future click([MediaButton button = MediaButton.media]) async { + switch (button) { + case MediaButton.media: + if (playbackState.value?.playing == true) { + await pause(); + } else { + await play(); + } + break; + case MediaButton.next: + await skipToNext(); + break; + case MediaButton.previous: + await skipToPrevious(); + break; + } + } + + @override + Future stop() async { + // await AudioService._stop(); + } + + @override + Future addQueueItem(MediaItem mediaItem) async {} + + @override + Future addQueueItems(List mediaItems) async {} + + @override + Future insertQueueItem(int index, MediaItem mediaItem) async {} + + @override + Future updateQueue(List queue) async {} + + @override + Future updateMediaItem(MediaItem mediaItem) async {} + + @override + Future removeQueueItem(MediaItem mediaItem) async {} + + @override + Future removeQueueItemAt(int index) async {} + + @override + Future skipToNext() async {} + + @override + Future skipToPrevious() async {} + + @override + Future fastForward() async {} + + @override + Future rewind() async {} + + @override + Future skipToQueueItem(int index) async {} + + @override + Future seek(Duration position) async {} + + @override + Future setRating(Rating rating, Map? extras) async {} + + @override + Future setCaptioningEnabled(bool enabled) async {} + + @override + Future setRepeatMode(AudioServiceRepeatMode repeatMode) async {} + + @override + Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async {} + + @override + Future seekBackward(bool begin) async {} + + @override + Future seekForward(bool begin) async {} + + @override + Future setSpeed(double speed) async {} + + @override + Future customAction( + String name, Map? arguments) async {} + + @override + Future onTaskRemoved() async {} + + @override + Future onNotificationDeleted() async { + await stop(); + } + + @override + Future> getChildren(String parentMediaId, + [Map? options]) async => + []; + + @override + ValueStream> subscribeToChildren(String parentMediaId) => + BehaviorSubject.seeded({}); + + @override + Future getMediaItem(String mediaId) async => null; + + @override + Future> search(String query, + [Map? extras]) async => + []; + + @override + Future androidAdjustRemoteVolume( + AndroidVolumeDirection direction) async {} + + @override + Future androidSetRemoteVolume(int volumeIndex) async {} + + @override + Stream get customEvent => customEventSubject.stream; +} + +/// 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 { + /// The amount to seek forward. + Duration get fastForwardInterval; + + /// The amount to seek back. + Duration get rewindInterval; + + _Seeker? _seeker; + + @override + Future fastForward() => _seekRelative(fastForwardInterval); + + @override + Future rewind() => _seekRelative(-rewindInterval); + + @override + Future seekForward(bool begin) async => _seekContinuously(begin, 1); + + @override + Future seekBackward(bool begin) async => _seekContinuously(begin, -1); + + /// Jumps away from the current position by [offset]. + Future _seekRelative(Duration offset) async { + var newPosition = playbackState.value!.position + offset; + // Make sure we don't jump out of bounds. + if (newPosition < Duration.zero) { + newPosition = Duration.zero; + } + final duration = mediaItem.value?.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.value?.duration != null) { + _seeker = _Seeker(this, Duration(seconds: 10 * direction), + Duration(seconds: 1), mediaItem.value!.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, + ); + + start() async { + _running = true; + while (_running) { + Duration newPosition = + handler.playbackState.value!.position + positionInterval; + if (newPosition < Duration.zero) newPosition = Duration.zero; + if (newPosition > duration) newPosition = duration; + handler.seek(newPosition); + await Future.delayed(stepInterval); + } + } + + 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 addQueueItem(MediaItem mediaItem) async { + queue.add(queue.value!..add(mediaItem)); + await super.addQueueItem(mediaItem); + } + + @override + Future addQueueItems(List mediaItems) async { + queue.add(queue.value!..addAll(mediaItems)); + await super.addQueueItems(mediaItems); + } + + @override + Future insertQueueItem(int index, MediaItem mediaItem) async { + queue.add(queue.value!..insert(index, mediaItem)); + await super.insertQueueItem(index, mediaItem); + } + + @override + Future updateQueue(List queue) async { + this.queue.add( + this.queue.value!..replaceRange(0, this.queue.value!.length, queue)); + await super.updateQueue(queue); + } + + @override + Future updateMediaItem(MediaItem mediaItem) async { + this.queue.add( + this.queue.value!..[this.queue.value!.indexOf(mediaItem)] = mediaItem); + await super.updateMediaItem(mediaItem); + } + + @override + Future removeQueueItem(MediaItem mediaItem) async { + queue.add(this.queue.value!..remove(mediaItem)); + await super.removeQueueItem(mediaItem); + } + + @override + Future skipToNext() async { + await _skip(1); + await super.skipToNext(); + } + + @override + Future skipToPrevious() async { + await _skip(-1); + await super.skipToPrevious(); + } + + /// This should be overridden to instruct how to skip to the queue item at + /// [index]. By default, this will broadcast [index] as + /// [PlaybackState.queueIndex] via the [playbackState] stream, and will + /// broadcast [queue] element [index] via the stream [mediaItem]. Your + /// implementation may call super to reuse this default implementation, or + /// else provide equivalent behaviour. + @override + Future skipToQueueItem(int index) async { + playbackState.add(playbackState.value!.copyWith(queueIndex: index)); + mediaItem.add(queue.value![index]); + await super.skipToQueueItem(index); + } + + Future _skip(int offset) async { + final queue = this.queue.value!; + final index = playbackState.value!.queueIndex!; + if (index < 0 || index >= queue.length) return; + await skipToQueueItem(index + offset); + } +} + +/// The available shuffle modes for the queue. +enum AudioServiceShuffleMode { none, all, 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] 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, +} + +/// (Maybe) temporary. +extension AudioServiceValueStream on ValueStream { + @Deprecated('Use "this" instead. Will be removed before the release') + ValueStream get stream => this; +} + +class AndroidVolumeDirection { + static final lower = AndroidVolumeDirection(-1); + static final same = AndroidVolumeDirection(0); + static final raise = AndroidVolumeDirection(1); + static final values = { + -1: lower, + 0: same, + 1: raise, + }; + final int index; + + AndroidVolumeDirection(this.index); + + @override + String toString() => '$index'; +} + +enum AndroidVolumeControlType { fixed, relative, absolute } + +abstract class AndroidPlaybackInfo { + AndroidPlaybackInfoMessage toMessage(); + + @override + String toString() => '${toMessage().toMap()}'; +} + +class RemoteAndroidPlaybackInfo extends AndroidPlaybackInfo { + //final AndroidAudioAttributes audioAttributes; + final AndroidVolumeControlType volumeControlType; + final int maxVolume; + final int volume; + + RemoteAndroidPlaybackInfo({ + required this.volumeControlType, + required this.maxVolume, + required this.volume, + }); + + AndroidPlaybackInfo copyWith({ + AndroidVolumeControlType? volumeControlType, + int? maxVolume, + int? volume, + }) => + RemoteAndroidPlaybackInfo( + volumeControlType: volumeControlType ?? this.volumeControlType, + maxVolume: maxVolume ?? this.maxVolume, + volume: volume ?? this.volume, + ); + + @override + RemoteAndroidPlaybackInfoMessage toMessage() => + RemoteAndroidPlaybackInfoMessage( + volumeControlType: + AndroidVolumeControlTypeMessage.values[volumeControlType.index], + maxVolume: maxVolume, + volume: volume, + ); +} + +class LocalAndroidPlaybackInfo extends AndroidPlaybackInfo { + LocalAndroidPlaybackInfoMessage toMessage() => + LocalAndroidPlaybackInfoMessage(); +} diff --git a/audio_service_dart/pubspec.yaml b/audio_service_dart/pubspec.yaml new file mode 100644 index 00000000..f0f8d006 --- /dev/null +++ b/audio_service_dart/pubspec.yaml @@ -0,0 +1,25 @@ +name: audio_service_dart +description: Dart code for audio_service +version: 0.1.0 +homepage: https://github.com/ryanheise/audio_service/tree/master/audio_service_entities + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + # Use these deps for local development + # audio_service_platform_interface_entities: + # path: ../audio_service_platform_interface_entities + + # Use these deps when pushing to origin (for the benefit of testers) + audio_service_platform_interface_entities: + git: + url: https://github.com/ryanheise/audio_service.git + ref: one-isolate + path: audio_service_platform_interface_entities + + # Use these deps when publishing. + # audio_service_platform_interface_entities: ^0.1.0 + + meta: ^1.3.0 + rxdart: ^0.26.0 \ No newline at end of file diff --git a/audio_service_platform_interface/lib/audio_service_platform_interface.dart b/audio_service_platform_interface/lib/audio_service_platform_interface.dart index 803b55a1..bd150f7c 100644 --- a/audio_service_platform_interface/lib/audio_service_platform_interface.dart +++ b/audio_service_platform_interface/lib/audio_service_platform_interface.dart @@ -1,8 +1,11 @@ +import 'package:audio_service_platform_interface_entities/audio_service_platform_interface_entities.dart'; import 'package:flutter/material.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'method_channel_audio_service.dart'; +export 'package:audio_service_platform_interface_entities/audio_service_platform_interface_entities.dart'; + abstract class AudioServicePlatform extends PlatformInterface { /// Constructs an AudioServicePlatform. AudioServicePlatform() : super(token: _token); @@ -206,1071 +209,6 @@ abstract class AudioHandlerCallbacks { AndroidAdjustRemoteVolumeRequest request); } -/// The different states during audio processing. -enum AudioProcessingStateMessage { - idle, - loading, - buffering, - ready, - completed, - error, -} - -/// The actons associated with playing audio. -enum MediaActionMessage { - stop, - pause, - play, - rewind, - skipToPrevious, - skipToNext, - fastForward, - setRating, - seek, - playPause, - playFromMediaId, - playFromSearch, - skipToQueueItem, - playFromUri, - prepare, - prepareFromMediaId, - prepareFromSearch, - prepareFromUri, - setRepeatMode, - unused_1, - unused_2, - setShuffleMode, - seekBackward, - seekForward, -} - -class MediaControlMessage { - /// 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 MediaActionMessage action; - - const MediaControlMessage({ - required this.androidIcon, - required this.label, - required this.action, - }); - - Map toMap() => { - 'androidIcon': androidIcon, - 'label': label, - 'action': action.index, - }; -} - -/// 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 PlaybackStateMessage { - /// The audio processing state e.g. [BasicPlaybackState.buffering]. - final AudioProcessingStateMessage 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 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? 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. - /// [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 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 AudioServiceRepeatModeMessage repeatMode; - - /// The current shuffle mode. - final AudioServiceShuffleModeMessage 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()]. - PlaybackStateMessage({ - this.processingState = AudioProcessingStateMessage.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 = AudioServiceRepeatModeMessage.none, - this.shuffleMode = AudioServiceShuffleModeMessage.none, - this.captioningEnabled = false, - this.queueIndex, - }) : assert(androidCompactActionIndices == null || - androidCompactActionIndices.length <= 3), - this.updateTime = updateTime ?? DateTime.now(); - - factory PlaybackStateMessage.fromMap(Map map) => PlaybackStateMessage( - processingState: - AudioProcessingStateMessage.values[map['processingState']], - playing: map['playing'], - controls: [], - androidCompactActionIndices: null, - systemActions: (map['systemActions'] as List) - .map((dynamic action) => MediaActionMessage.values[action as int]) - .toSet(), - updatePosition: Duration(microseconds: map['updatePosition']), - bufferedPosition: Duration(microseconds: map['bufferedPosition']), - speed: map['speed'], - updateTime: DateTime.fromMillisecondsSinceEpoch(map['updateTime']), - errorCode: map['errorCode'], - errorMessage: map['errorMessage'], - repeatMode: AudioServiceRepeatModeMessage.values[map['repeatMode']], - shuffleMode: AudioServiceShuffleModeMessage.values[map['shuffleMode']], - captioningEnabled: map['captioningEnabled'], - queueIndex: map['queueIndex'], - ); - Map toMap() => { - 'processingState': processingState.index, - 'playing': playing, - 'controls': controls.map((control) => control.toMap()).toList(), - 'androidCompactActionIndices': androidCompactActionIndices, - 'systemActions': systemActions.map((action) => action.index).toList(), - 'updatePosition': updatePosition.inMilliseconds, - 'bufferedPosition': bufferedPosition.inMilliseconds, - 'speed': speed, - 'updateTime': updateTime.millisecondsSinceEpoch, - 'errorCode': errorCode, - 'errorMessage': errorMessage, - 'repeatMode': repeatMode.index, - 'shuffleMode': shuffleMode.index, - 'captioningEnabled': captioningEnabled, - 'queueIndex': queueIndex, - }; -} - -class AndroidVolumeDirectionMessage { - static final lower = AndroidVolumeDirectionMessage(-1); - static final same = AndroidVolumeDirectionMessage(0); - static final raise = AndroidVolumeDirectionMessage(1); - static final values = { - -1: lower, - 0: same, - 1: raise, - }; - final int index; - - AndroidVolumeDirectionMessage(this.index); - - @override - String toString() => '$index'; -} - -class AndroidPlaybackTypeMessage { - static final local = AndroidPlaybackTypeMessage(1); - static final remote = AndroidPlaybackTypeMessage(2); - final int index; - - AndroidPlaybackTypeMessage(this.index); - - @override - String toString() => '$index'; -} - -enum AndroidVolumeControlTypeMessage { fixed, relative, absolute } - -abstract class AndroidPlaybackInfoMessage { - Map toMap(); -} - -class RemoteAndroidPlaybackInfoMessage extends AndroidPlaybackInfoMessage { - //final AndroidAudioAttributes audioAttributes; - final AndroidVolumeControlTypeMessage volumeControlType; - final int maxVolume; - final int volume; - - RemoteAndroidPlaybackInfoMessage({ - required this.volumeControlType, - required this.maxVolume, - required this.volume, - }); - - Map toMap() => { - 'playbackType': AndroidPlaybackTypeMessage.remote.index, - 'volumeControlType': volumeControlType.index, - 'maxVolume': maxVolume, - 'volume': volume, - }; - - @override - String toString() => '${toMap()}'; -} - -class LocalAndroidPlaybackInfoMessage extends AndroidPlaybackInfoMessage { - Map toMap() => { - 'playbackType': AndroidPlaybackTypeMessage.local.index, - }; - - @override - String toString() => '${toMap()}'; -} - -/// The different buttons on a headset. -enum MediaButtonMessage { - media, - next, - previous, -} - -/// The available shuffle modes for the queue. -enum AudioServiceShuffleModeMessage { none, all, group } - -/// The available repeat modes. -/// -/// This defines how media items should repeat when the current one is finished. -enum AudioServiceRepeatModeMessage { - /// 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] 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, -} - -class MediaItemMessage { - /// A unique id. - final String id; - - /// The album this media item belongs to. - final String album; - - /// The title of this media item. - final String title; - - /// 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 for this media item as a uri. - final Uri? artUri; - - /// 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 MediaItemMessage. - final RatingMessage? rating; - - /// A map of additional metadata for the media item. - /// - /// The values must be integers or strings. - final Map? extras; - - /// Creates a [MediaItemMessage]. - /// - /// [id], [album] and [title] must not be null, and [id] must be unique for - /// each instance. - const MediaItemMessage({ - required this.id, - required this.album, - required this.title, - this.artist, - this.genre, - this.duration, - this.artUri, - this.playable = true, - this.displayTitle, - this.displaySubtitle, - this.displayDescription, - this.rating, - this.extras, - }); - - /// Creates a [MediaItemMessage] from a map of key/value pairs corresponding to - /// fields of this class. - factory MediaItemMessage.fromMap(Map raw) => MediaItemMessage( - id: raw['id'], - album: raw['album'], - title: raw['title'], - artist: raw['artist'], - genre: raw['genre'], - duration: raw['duration'] != null - ? Duration(milliseconds: raw['duration']) - : null, - artUri: raw['artUri'] != null ? Uri.parse(raw['artUri']) : null, - playable: raw['playable'], - displayTitle: raw['displayTitle'], - displaySubtitle: raw['displaySubtitle'], - displayDescription: raw['displayDescription'], - rating: - raw['rating'] != null ? RatingMessage.fromMap(raw['rating']) : null, - extras: (raw['extras'] as Map?)?.cast(), - ); - - /// Converts this [MediaItemMessage] to a map of key/value pairs corresponding to - /// the fields of this class. - Map toMap() => { - 'id': id, - 'album': album, - 'title': title, - 'artist': artist, - 'genre': genre, - 'duration': duration?.inMilliseconds, - 'artUri': artUri?.toString(), - 'playable': playable, - 'displayTitle': displayTitle, - 'displaySubtitle': displaySubtitle, - 'displayDescription': displayDescription, - 'rating': rating?.toMap(), - 'extras': extras, - }; -} - -/// A rating to attach to a MediaItemMessage. -class RatingMessage { - final RatingStyleMessage type; - final dynamic value; - - const RatingMessage({required this.type, required this.value}); - - /// Returns a percentage rating value greater or equal to 0.0f, or a - /// negative value if the rating style is not percentage-based, or - /// if it is unrated. - double get percentRating { - if (type != RatingStyleMessage.percentage) return -1; - if (value < 0 || value > 100) return -1; - return value ?? -1; - } - - /// Returns a rating value greater or equal to 0.0f, or a negative - /// value if the rating style is not star-based, or if it is - /// unrated. - int get starRating { - if (type != RatingStyleMessage.range3stars && - type != RatingStyleMessage.range4stars && - type != RatingStyleMessage.range5stars) return -1; - return value ?? -1; - } - - /// Returns true if the rating is "heart selected" or false if the - /// rating is "heart unselected", if the rating style is not [heart] - /// or if it is unrated. - bool get hasHeart { - if (type != RatingStyleMessage.heart) return false; - return value ?? false; - } - - /// Returns true if the rating is "thumb up" or false if the rating - /// is "thumb down", if the rating style is not [thumbUpDown] or if - /// it is unrated. - bool get isThumbUp { - if (type != RatingStyleMessage.thumbUpDown) return false; - return value ?? false; - } - - /// Return whether there is a rating value available. - bool get isRated => value != null; - - Map toMap() { - return { - 'type': type.index, - 'value': value, - }; - } - - // Even though this should take a Map, that makes an error. - RatingMessage.fromMap(Map raw) - : this(type: RatingStyleMessage.values[raw['type']], value: raw['value']); - - @override - String toString() => '${toMap()}'; -} - -enum RatingStyleMessage { - /// 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, -} - -class OnPlaybackStateChangedRequest { - final PlaybackStateMessage state; - - OnPlaybackStateChangedRequest({ - required this.state, - }); - - factory OnPlaybackStateChangedRequest.fromMap(Map map) => - OnPlaybackStateChangedRequest( - state: PlaybackStateMessage.fromMap(map['state'])); - - Map toMap() => { - 'state': state.toMap(), - }; -} - -class OnQueueChangedRequest { - final List queue; - - OnQueueChangedRequest({ - required this.queue, - }); - - factory OnQueueChangedRequest.fromMap(Map map) => OnQueueChangedRequest( - queue: map['queue'] == null - ? [] - : (map['queue'] as List) - .map((raw) => MediaItemMessage.fromMap(raw)) - .toList()); - - Map toMap() => { - 'queue': queue.map((item) => item.toMap()).toList(), - }; -} - -class OnMediaItemChangedRequest { - final MediaItemMessage? mediaItem; - - OnMediaItemChangedRequest({ - required this.mediaItem, - }); - - factory OnMediaItemChangedRequest.fromMap(Map map) => - OnMediaItemChangedRequest( - mediaItem: map['mediaItem'] == null - ? null - : MediaItemMessage.fromMap(map['mediaItem']), - ); - - Map toMap() => { - 'mediaItem': mediaItem?.toMap(), - }; -} - -class OnChildrenLoadedRequest { - final String parentMediaId; - final List children; - - OnChildrenLoadedRequest({ - required this.parentMediaId, - required this.children, - }); - - factory OnChildrenLoadedRequest.fromMap(Map map) => OnChildrenLoadedRequest( - parentMediaId: map['parentMediaId'], - children: (map['queue'] as List) - .map((raw) => MediaItemMessage.fromMap(raw)) - .toList(), - ); -} - -class OnNotificationClickedRequest { - final bool clicked; - - OnNotificationClickedRequest({ - required this.clicked, - }); - - factory OnNotificationClickedRequest.fromMap(Map map) => - OnNotificationClickedRequest( - clicked: map['clicked'] == null, - ); - - Map toMap() => { - 'clicked': clicked, - }; -} - -class SetStateRequest { - final PlaybackStateMessage state; - - SetStateRequest({ - required this.state, - }); - - Map toMap() => { - 'state': state.toMap(), - }; -} - -class SetQueueRequest { - final List queue; - - SetQueueRequest({required this.queue}); - - Map toMap() => { - 'queue': queue.map((item) => item.toMap()).toList(), - }; -} - -class SetMediaItemRequest { - final MediaItemMessage mediaItem; - - SetMediaItemRequest({required this.mediaItem}); - - Map toMap() => { - 'mediaItem': mediaItem.toMap(), - }; -} - -class StopServiceRequest { - Map toMap() => {}; -} - -class SetAndroidPlaybackInfoRequest { - final AndroidPlaybackInfoMessage playbackInfo; - - SetAndroidPlaybackInfoRequest({required this.playbackInfo}); - - Map toMap() => { - 'playbackInfo': playbackInfo.toMap(), - }; -} - -class AndroidForceEnableMediaButtonsRequest { - Map toMap() => {}; -} - -class NotifyChildrenChangedRequest { - final String parentMediaId; - final Map? options; - - NotifyChildrenChangedRequest({required this.parentMediaId, this.options}); - - Map toMap() => { - 'parentMediaId': parentMediaId, - 'options': options, - }; -} - -class PrepareRequest { - Map toMap() => {}; -} - -class PrepareFromMediaIdRequest { - final String mediaId; - final Map? extras; - - PrepareFromMediaIdRequest({required this.mediaId, this.extras}); - - Map toMap() => { - 'mediaId': mediaId, - }; -} - -class PrepareFromSearchRequest { - final String query; - final Map? extras; - - PrepareFromSearchRequest({required this.query, this.extras}); - - Map toMap() => { - 'query': query, - 'extras': extras, - }; -} - -class PrepareFromUriRequest { - final Uri uri; - final Map? extras; - - PrepareFromUriRequest({required this.uri, this.extras}); - - Map toMap() => { - 'uri': uri.toString(), - 'extras': extras, - }; -} - -class PlayRequest { - Map toMap() => {}; -} - -class PlayFromMediaIdRequest { - final String mediaId; - final Map? extras; - - PlayFromMediaIdRequest({required this.mediaId, this.extras}); - - Map toMap() => { - 'mediaId': mediaId, - }; -} - -class PlayFromSearchRequest { - final String query; - final Map? extras; - - PlayFromSearchRequest({required this.query, this.extras}); - - Map toMap() => { - 'query': query, - 'extras': extras, - }; -} - -class PlayFromUriRequest { - final Uri uri; - final Map? extras; - - PlayFromUriRequest({required this.uri, this.extras}); - - Map toMap() => { - 'uri': uri.toString(), - 'extras': extras, - }; -} - -class PlayMediaItemRequest { - final MediaItemMessage mediaItem; - - PlayMediaItemRequest({required this.mediaItem}); - - Map toMap() => { - 'mediaItem': mediaItem.toString(), - }; -} - -class PauseRequest { - Map toMap() => {}; -} - -class ClickRequest { - final MediaButtonMessage button; - - ClickRequest({required this.button}); - - Map toMap() => { - 'button': button.index, - }; -} - -class StopRequest { - Map toMap() => {}; -} - -class AddQueueItemRequest { - final MediaItemMessage mediaItem; - - AddQueueItemRequest({required this.mediaItem}); - - Map toMap() => { - 'mediaItem': mediaItem.toMap(), - }; -} - -class AddQueueItemsRequest { - final List queue; - - AddQueueItemsRequest({required this.queue}); - - Map toMap() => { - 'queue': queue.map((item) => item.toMap()).toList(), - }; -} - -class InsertQueueItemRequest { - final int index; - final MediaItemMessage mediaItem; - - InsertQueueItemRequest({required this.index, required this.mediaItem}); - - Map toMap() => { - 'index': index, - 'mediaItem': mediaItem.toMap(), - }; -} - -class UpdateQueueRequest { - final List queue; - - UpdateQueueRequest({required this.queue}); - - Map toMap() => { - 'queue': queue.map((item) => item.toMap()).toList(), - }; -} - -class UpdateMediaItemRequest { - final MediaItemMessage mediaItem; - - UpdateMediaItemRequest({required this.mediaItem}); - - Map toMap() => { - 'mediaItem': mediaItem.toMap(), - }; -} - -class RemoveQueueItemRequest { - final MediaItemMessage mediaItem; - - RemoveQueueItemRequest({required this.mediaItem}); - - Map toMap() => { - 'mediaItem': mediaItem.toMap(), - }; -} - -class RemoveQueueItemAtRequest { - final int index; - - RemoveQueueItemAtRequest({required this.index}); - - Map toMap() => { - 'index': index, - }; -} - -class SkipToNextRequest { - Map toMap() => {}; -} - -class SkipToPreviousRequest { - Map toMap() => {}; -} - -class FastForwardRequest { - Map toMap() => {}; -} - -class RewindRequest { - Map toMap() => {}; -} - -class SkipToQueueItemRequest { - final int index; - - SkipToQueueItemRequest({required this.index}); - - Map toMap() => { - 'index': index, - }; -} - -class SeekRequest { - final Duration position; - - SeekRequest({required this.position}); - - Map toMap() => { - 'position': position.inMicroseconds, - }; -} - -class SetRatingRequest { - final RatingMessage rating; - final Map? extras; - - SetRatingRequest({required this.rating, this.extras}); - - Map toMap() => { - 'rating': rating.toMap(), - 'extras': extras, - }; -} - -class SetCaptioningEnabledRequest { - final bool enabled; - - SetCaptioningEnabledRequest({required this.enabled}); - - Map toMap() => { - 'enabled': enabled, - }; -} - -class SetRepeatModeRequest { - final AudioServiceRepeatModeMessage repeatMode; - - SetRepeatModeRequest({required this.repeatMode}); - - Map toMap() => { - 'repeatMode': repeatMode.index, - }; -} - -class SetShuffleModeRequest { - final AudioServiceShuffleModeMessage shuffleMode; - - SetShuffleModeRequest({required this.shuffleMode}); - - Map toMap() => { - 'shuffleMode': shuffleMode.index, - }; -} - -class SeekBackwardRequest { - final bool begin; - - SeekBackwardRequest({required this.begin}); - - Map toMap() => { - 'begin': begin, - }; -} - -class SeekForwardRequest { - final bool begin; - - SeekForwardRequest({required this.begin}); - - Map toMap() => { - 'begin': begin, - }; -} - -class SetSpeedRequest { - final double speed; - - SetSpeedRequest({required this.speed}); - - Map toMap() => { - 'speed': speed, - }; -} - -class CustomActionRequest { - final String name; - final Map? extras; - - CustomActionRequest({required this.name, this.extras}); - - Map toMap() => { - 'name': name, - 'extras': extras, - }; -} - -class OnTaskRemovedRequest { - Map toMap() => {}; -} - -class OnNotificationDeletedRequest { - Map toMap() => {}; -} - -class GetChildrenRequest { - final String parentMediaId; - final Map? options; - - GetChildrenRequest({required this.parentMediaId, this.options}); - - Map toMap() => { - 'parentMediaId': parentMediaId, - 'options': options, - }; -} - -class GetChildrenResponse { - final List children; - - GetChildrenResponse({required this.children}); - - Map toMap() => { - 'children': children.map((item) => item.toMap()).toList(), - }; -} - -class GetMediaItemRequest { - final String mediaId; - - GetMediaItemRequest({required this.mediaId}); - - Map toMap() => { - 'mediaId': mediaId, - }; -} - -class GetMediaItemResponse { - final MediaItemMessage? mediaItem; - - GetMediaItemResponse({required this.mediaItem}); - - Map toMap() => { - 'mediaItem': mediaItem?.toMap(), - }; -} - -class SearchRequest { - final String query; - final Map? extras; - - SearchRequest({required this.query, this.extras}); - - Map toMap() => { - 'query': query, - 'extras': extras, - }; -} - -class SearchResponse { - final List mediaItems; - - SearchResponse({required this.mediaItems}); - - Map toMap() => { - 'mediaItems': mediaItems.map((item) => item.toMap()).toList(), - }; -} - -class AndroidSetRemoteVolumeRequest { - final int volumeIndex; - - AndroidSetRemoteVolumeRequest({required this.volumeIndex}); - - Map toMap() => { - 'volumeIndex': volumeIndex, - }; -} - -class AndroidAdjustRemoteVolumeRequest { - final AndroidVolumeDirectionMessage direction; - - AndroidAdjustRemoteVolumeRequest({required this.direction}); - - Map toMap() => { - 'direction': direction.index, - }; -} - -class ConfigureRequest { - final AudioServiceConfigMessage config; - - ConfigureRequest({ - required this.config, - }); - - Map toMap() => { - 'config': config.toMap(), - }; -} - -class ConfigureResponse { - static ConfigureResponse fromMap(Map map) => - ConfigureResponse(); -} - class AudioServiceConfigMessage { final bool androidResumeOnClick; final String androidNotificationChannelName; @@ -1370,3 +308,20 @@ class AudioServiceConfigMessage { 'androidBrowsableRootExtras': androidBrowsableRootExtras, }; } + +class ConfigureRequest { + final AudioServiceConfigMessage config; + + ConfigureRequest({ + required this.config, + }); + + Map toMap() => { + 'config': config.toMap(), + }; +} + +class ConfigureResponse { + static ConfigureResponse fromMap(Map map) => + ConfigureResponse(); +} diff --git a/audio_service_platform_interface/pubspec.yaml b/audio_service_platform_interface/pubspec.yaml index df0634ea..46d284d2 100644 --- a/audio_service_platform_interface/pubspec.yaml +++ b/audio_service_platform_interface/pubspec.yaml @@ -10,6 +10,20 @@ dependencies: sdk: flutter plugin_platform_interface: ^2.0.0 + # Use these deps for local development + # audio_service_platform_interface_entities: + # path: ../audio_service_platform_interface_entities + + # Use these deps when pushing to origin (for the benefit of testers) + audio_service_platform_interface_entities: + git: + url: https://github.com/ryanheise/audio_service.git + ref: one-isolate + path: audio_service_platform_interface_entities + + # Use these deps when publishing. + # audio_service_platform_interface_entities: ^0.1.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/audio_service_platform_interface_entities/.gitignore b/audio_service_platform_interface_entities/.gitignore new file mode 100644 index 00000000..0c44ab06 --- /dev/null +++ b/audio_service_platform_interface_entities/.gitignore @@ -0,0 +1,13 @@ +# Files and directories created by pub +.dart_tool/ +.packages + +# Omit commiting pubspec.lock for library packages: +# https://dart.dev/guides/libraries/private-files#pubspeclock +pubspec.lock + +# Conventional directory for build outputs +build/ + +# Directory created by dartdoc +doc/api/ diff --git a/audio_service_platform_interface_entities/CHANGELOG.md b/audio_service_platform_interface_entities/CHANGELOG.md new file mode 100644 index 00000000..13187808 --- /dev/null +++ b/audio_service_platform_interface_entities/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release diff --git a/audio_service_platform_interface_entities/LICENSE b/audio_service_platform_interface_entities/LICENSE new file mode 100644 index 00000000..971b99d6 --- /dev/null +++ b/audio_service_platform_interface_entities/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ryan Heise and the project contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/audio_service_platform_interface_entities/lib/audio_service_platform_interface_entities.dart b/audio_service_platform_interface_entities/lib/audio_service_platform_interface_entities.dart new file mode 100644 index 00000000..2e61af3e --- /dev/null +++ b/audio_service_platform_interface_entities/lib/audio_service_platform_interface_entities.dart @@ -0,0 +1,3 @@ +library audio_service_platform_interface_entities; + +export 'src/audio_service_platform_interface_entities.dart'; diff --git a/audio_service_platform_interface_entities/lib/src/audio_service_platform_interface_entities.dart b/audio_service_platform_interface_entities/lib/src/audio_service_platform_interface_entities.dart new file mode 100644 index 00000000..6a26bf1e --- /dev/null +++ b/audio_service_platform_interface_entities/lib/src/audio_service_platform_interface_entities.dart @@ -0,0 +1,1047 @@ +/// The different states during audio processing. +enum AudioProcessingStateMessage { + idle, + loading, + buffering, + ready, + completed, + error, +} + +/// The actons associated with playing audio. +enum MediaActionMessage { + stop, + pause, + play, + rewind, + skipToPrevious, + skipToNext, + fastForward, + setRating, + seek, + playPause, + playFromMediaId, + playFromSearch, + skipToQueueItem, + playFromUri, + prepare, + prepareFromMediaId, + prepareFromSearch, + prepareFromUri, + setRepeatMode, + unused_1, + unused_2, + setShuffleMode, + seekBackward, + seekForward, +} + +class MediaControlMessage { + /// 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 MediaActionMessage action; + + const MediaControlMessage({ + required this.androidIcon, + required this.label, + required this.action, + }); + + Map toMap() => { + 'androidIcon': androidIcon, + 'label': label, + 'action': action.index, + }; +} + +/// 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 PlaybackStateMessage { + /// The audio processing state e.g. [BasicPlaybackState.buffering]. + final AudioProcessingStateMessage 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 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? 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. + /// [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 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 AudioServiceRepeatModeMessage repeatMode; + + /// The current shuffle mode. + final AudioServiceShuffleModeMessage 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()]. + PlaybackStateMessage({ + this.processingState = AudioProcessingStateMessage.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 = AudioServiceRepeatModeMessage.none, + this.shuffleMode = AudioServiceShuffleModeMessage.none, + this.captioningEnabled = false, + this.queueIndex, + }) : assert(androidCompactActionIndices == null || + androidCompactActionIndices.length <= 3), + this.updateTime = updateTime ?? DateTime.now(); + + factory PlaybackStateMessage.fromMap(Map map) => PlaybackStateMessage( + processingState: + AudioProcessingStateMessage.values[map['processingState']], + playing: map['playing'], + controls: [], + androidCompactActionIndices: null, + systemActions: (map['systemActions'] as List) + .map((dynamic action) => MediaActionMessage.values[action as int]) + .toSet(), + updatePosition: Duration(microseconds: map['updatePosition']), + bufferedPosition: Duration(microseconds: map['bufferedPosition']), + speed: map['speed'], + updateTime: DateTime.fromMillisecondsSinceEpoch(map['updateTime']), + errorCode: map['errorCode'], + errorMessage: map['errorMessage'], + repeatMode: AudioServiceRepeatModeMessage.values[map['repeatMode']], + shuffleMode: AudioServiceShuffleModeMessage.values[map['shuffleMode']], + captioningEnabled: map['captioningEnabled'], + queueIndex: map['queueIndex'], + ); + Map toMap() => { + 'processingState': processingState.index, + 'playing': playing, + 'controls': controls.map((control) => control.toMap()).toList(), + 'androidCompactActionIndices': androidCompactActionIndices, + 'systemActions': systemActions.map((action) => action.index).toList(), + 'updatePosition': updatePosition.inMilliseconds, + 'bufferedPosition': bufferedPosition.inMilliseconds, + 'speed': speed, + 'updateTime': updateTime.millisecondsSinceEpoch, + 'errorCode': errorCode, + 'errorMessage': errorMessage, + 'repeatMode': repeatMode.index, + 'shuffleMode': shuffleMode.index, + 'captioningEnabled': captioningEnabled, + 'queueIndex': queueIndex, + }; +} + +class AndroidVolumeDirectionMessage { + static final lower = AndroidVolumeDirectionMessage(-1); + static final same = AndroidVolumeDirectionMessage(0); + static final raise = AndroidVolumeDirectionMessage(1); + static final values = { + -1: lower, + 0: same, + 1: raise, + }; + final int index; + + AndroidVolumeDirectionMessage(this.index); + + @override + String toString() => '$index'; +} + +class AndroidPlaybackTypeMessage { + static final local = AndroidPlaybackTypeMessage(1); + static final remote = AndroidPlaybackTypeMessage(2); + final int index; + + AndroidPlaybackTypeMessage(this.index); + + @override + String toString() => '$index'; +} + +enum AndroidVolumeControlTypeMessage { fixed, relative, absolute } + +abstract class AndroidPlaybackInfoMessage { + Map toMap(); +} + +class RemoteAndroidPlaybackInfoMessage extends AndroidPlaybackInfoMessage { + //final AndroidAudioAttributes audioAttributes; + final AndroidVolumeControlTypeMessage volumeControlType; + final int maxVolume; + final int volume; + + RemoteAndroidPlaybackInfoMessage({ + required this.volumeControlType, + required this.maxVolume, + required this.volume, + }); + + Map toMap() => { + 'playbackType': AndroidPlaybackTypeMessage.remote.index, + 'volumeControlType': volumeControlType.index, + 'maxVolume': maxVolume, + 'volume': volume, + }; + + @override + String toString() => '${toMap()}'; +} + +class LocalAndroidPlaybackInfoMessage extends AndroidPlaybackInfoMessage { + Map toMap() => { + 'playbackType': AndroidPlaybackTypeMessage.local.index, + }; + + @override + String toString() => '${toMap()}'; +} + +/// The different buttons on a headset. +enum MediaButtonMessage { + media, + next, + previous, +} + +/// The available shuffle modes for the queue. +enum AudioServiceShuffleModeMessage { none, all, group } + +/// The available repeat modes. +/// +/// This defines how media items should repeat when the current one is finished. +enum AudioServiceRepeatModeMessage { + /// 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] 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, +} + +class MediaItemMessage { + /// A unique id. + final String id; + + /// The album this media item belongs to. + final String album; + + /// The title of this media item. + final String title; + + /// 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 for this media item as a uri. + final Uri? artUri; + + /// 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 MediaItemMessage. + final RatingMessage? rating; + + /// A map of additional metadata for the media item. + /// + /// The values must be integers or strings. + final Map? extras; + + /// Creates a [MediaItemMessage]. + /// + /// [id], [album] and [title] must not be null, and [id] must be unique for + /// each instance. + const MediaItemMessage({ + required this.id, + required this.album, + required this.title, + this.artist, + this.genre, + this.duration, + this.artUri, + this.playable = true, + this.displayTitle, + this.displaySubtitle, + this.displayDescription, + this.rating, + this.extras, + }); + + /// Creates a [MediaItemMessage] from a map of key/value pairs corresponding to + /// fields of this class. + factory MediaItemMessage.fromMap(Map raw) => MediaItemMessage( + id: raw['id'], + album: raw['album'], + title: raw['title'], + artist: raw['artist'], + genre: raw['genre'], + duration: raw['duration'] != null + ? Duration(milliseconds: raw['duration']) + : null, + artUri: raw['artUri'] != null ? Uri.parse(raw['artUri']) : null, + playable: raw['playable'], + displayTitle: raw['displayTitle'], + displaySubtitle: raw['displaySubtitle'], + displayDescription: raw['displayDescription'], + rating: + raw['rating'] != null ? RatingMessage.fromMap(raw['rating']) : null, + extras: (raw['extras'] as Map?)?.cast(), + ); + + /// Converts this [MediaItemMessage] to a map of key/value pairs corresponding to + /// the fields of this class. + Map toMap() => { + 'id': id, + 'album': album, + 'title': title, + 'artist': artist, + 'genre': genre, + 'duration': duration?.inMilliseconds, + 'artUri': artUri?.toString(), + 'playable': playable, + 'displayTitle': displayTitle, + 'displaySubtitle': displaySubtitle, + 'displayDescription': displayDescription, + 'rating': rating?.toMap(), + 'extras': extras, + }; +} + +/// A rating to attach to a MediaItemMessage. +class RatingMessage { + final RatingStyleMessage type; + final dynamic value; + + const RatingMessage({required this.type, required this.value}); + + /// Returns a percentage rating value greater or equal to 0.0f, or a + /// negative value if the rating style is not percentage-based, or + /// if it is unrated. + double get percentRating { + if (type != RatingStyleMessage.percentage) return -1; + if (value < 0 || value > 100) return -1; + return value ?? -1; + } + + /// Returns a rating value greater or equal to 0.0f, or a negative + /// value if the rating style is not star-based, or if it is + /// unrated. + int get starRating { + if (type != RatingStyleMessage.range3stars && + type != RatingStyleMessage.range4stars && + type != RatingStyleMessage.range5stars) return -1; + return value ?? -1; + } + + /// Returns true if the rating is "heart selected" or false if the + /// rating is "heart unselected", if the rating style is not [heart] + /// or if it is unrated. + bool get hasHeart { + if (type != RatingStyleMessage.heart) return false; + return value ?? false; + } + + /// Returns true if the rating is "thumb up" or false if the rating + /// is "thumb down", if the rating style is not [thumbUpDown] or if + /// it is unrated. + bool get isThumbUp { + if (type != RatingStyleMessage.thumbUpDown) return false; + return value ?? false; + } + + /// Return whether there is a rating value available. + bool get isRated => value != null; + + Map toMap() { + return { + 'type': type.index, + 'value': value, + }; + } + + // Even though this should take a Map, that makes an error. + RatingMessage.fromMap(Map raw) + : this(type: RatingStyleMessage.values[raw['type']], value: raw['value']); + + @override + String toString() => '${toMap()}'; +} + +enum RatingStyleMessage { + /// 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, +} + +class OnPlaybackStateChangedRequest { + final PlaybackStateMessage state; + + OnPlaybackStateChangedRequest({ + required this.state, + }); + + factory OnPlaybackStateChangedRequest.fromMap(Map map) => + OnPlaybackStateChangedRequest( + state: PlaybackStateMessage.fromMap(map['state'])); + + Map toMap() => { + 'state': state.toMap(), + }; +} + +class OnQueueChangedRequest { + final List queue; + + OnQueueChangedRequest({ + required this.queue, + }); + + factory OnQueueChangedRequest.fromMap(Map map) => OnQueueChangedRequest( + queue: map['queue'] == null + ? [] + : (map['queue'] as List) + .map((raw) => MediaItemMessage.fromMap(raw)) + .toList()); + + Map toMap() => { + 'queue': queue.map((item) => item.toMap()).toList(), + }; +} + +class OnMediaItemChangedRequest { + final MediaItemMessage? mediaItem; + + OnMediaItemChangedRequest({ + required this.mediaItem, + }); + + factory OnMediaItemChangedRequest.fromMap(Map map) => + OnMediaItemChangedRequest( + mediaItem: map['mediaItem'] == null + ? null + : MediaItemMessage.fromMap(map['mediaItem']), + ); + + Map toMap() => { + 'mediaItem': mediaItem?.toMap(), + }; +} + +class OnChildrenLoadedRequest { + final String parentMediaId; + final List children; + + OnChildrenLoadedRequest({ + required this.parentMediaId, + required this.children, + }); + + factory OnChildrenLoadedRequest.fromMap(Map map) => OnChildrenLoadedRequest( + parentMediaId: map['parentMediaId'], + children: (map['queue'] as List) + .map((raw) => MediaItemMessage.fromMap(raw)) + .toList(), + ); +} + +class OnNotificationClickedRequest { + final bool clicked; + + OnNotificationClickedRequest({ + required this.clicked, + }); + + factory OnNotificationClickedRequest.fromMap(Map map) => + OnNotificationClickedRequest( + clicked: map['clicked'] == null, + ); + + Map toMap() => { + 'clicked': clicked, + }; +} + +class SetStateRequest { + final PlaybackStateMessage state; + + SetStateRequest({ + required this.state, + }); + + Map toMap() => { + 'state': state.toMap(), + }; +} + +class SetQueueRequest { + final List queue; + + SetQueueRequest({required this.queue}); + + Map toMap() => { + 'queue': queue.map((item) => item.toMap()).toList(), + }; +} + +class SetMediaItemRequest { + final MediaItemMessage mediaItem; + + SetMediaItemRequest({required this.mediaItem}); + + Map toMap() => { + 'mediaItem': mediaItem.toMap(), + }; +} + +class StopServiceRequest { + Map toMap() => {}; +} + +class SetAndroidPlaybackInfoRequest { + final AndroidPlaybackInfoMessage playbackInfo; + + SetAndroidPlaybackInfoRequest({required this.playbackInfo}); + + Map toMap() => { + 'playbackInfo': playbackInfo.toMap(), + }; +} + +class AndroidForceEnableMediaButtonsRequest { + Map toMap() => {}; +} + +class NotifyChildrenChangedRequest { + final String parentMediaId; + final Map? options; + + NotifyChildrenChangedRequest({required this.parentMediaId, this.options}); + + Map toMap() => { + 'parentMediaId': parentMediaId, + 'options': options, + }; +} + +class PrepareRequest { + Map toMap() => {}; +} + +class PrepareFromMediaIdRequest { + final String mediaId; + final Map? extras; + + PrepareFromMediaIdRequest({required this.mediaId, this.extras}); + + Map toMap() => { + 'mediaId': mediaId, + }; +} + +class PrepareFromSearchRequest { + final String query; + final Map? extras; + + PrepareFromSearchRequest({required this.query, this.extras}); + + Map toMap() => { + 'query': query, + 'extras': extras, + }; +} + +class PrepareFromUriRequest { + final Uri uri; + final Map? extras; + + PrepareFromUriRequest({required this.uri, this.extras}); + + Map toMap() => { + 'uri': uri.toString(), + 'extras': extras, + }; +} + +class PlayRequest { + Map toMap() => {}; +} + +class PlayFromMediaIdRequest { + final String mediaId; + final Map? extras; + + PlayFromMediaIdRequest({required this.mediaId, this.extras}); + + Map toMap() => { + 'mediaId': mediaId, + }; +} + +class PlayFromSearchRequest { + final String query; + final Map? extras; + + PlayFromSearchRequest({required this.query, this.extras}); + + Map toMap() => { + 'query': query, + 'extras': extras, + }; +} + +class PlayFromUriRequest { + final Uri uri; + final Map? extras; + + PlayFromUriRequest({required this.uri, this.extras}); + + Map toMap() => { + 'uri': uri.toString(), + 'extras': extras, + }; +} + +class PlayMediaItemRequest { + final MediaItemMessage mediaItem; + + PlayMediaItemRequest({required this.mediaItem}); + + Map toMap() => { + 'mediaItem': mediaItem.toString(), + }; +} + +class PauseRequest { + Map toMap() => {}; +} + +class ClickRequest { + final MediaButtonMessage button; + + ClickRequest({required this.button}); + + Map toMap() => { + 'button': button.index, + }; +} + +class StopRequest { + Map toMap() => {}; +} + +class AddQueueItemRequest { + final MediaItemMessage mediaItem; + + AddQueueItemRequest({required this.mediaItem}); + + Map toMap() => { + 'mediaItem': mediaItem.toMap(), + }; +} + +class AddQueueItemsRequest { + final List queue; + + AddQueueItemsRequest({required this.queue}); + + Map toMap() => { + 'queue': queue.map((item) => item.toMap()).toList(), + }; +} + +class InsertQueueItemRequest { + final int index; + final MediaItemMessage mediaItem; + + InsertQueueItemRequest({required this.index, required this.mediaItem}); + + Map toMap() => { + 'index': index, + 'mediaItem': mediaItem.toMap(), + }; +} + +class UpdateQueueRequest { + final List queue; + + UpdateQueueRequest({required this.queue}); + + Map toMap() => { + 'queue': queue.map((item) => item.toMap()).toList(), + }; +} + +class UpdateMediaItemRequest { + final MediaItemMessage mediaItem; + + UpdateMediaItemRequest({required this.mediaItem}); + + Map toMap() => { + 'mediaItem': mediaItem.toMap(), + }; +} + +class RemoveQueueItemRequest { + final MediaItemMessage mediaItem; + + RemoveQueueItemRequest({required this.mediaItem}); + + Map toMap() => { + 'mediaItem': mediaItem.toMap(), + }; +} + +class RemoveQueueItemAtRequest { + final int index; + + RemoveQueueItemAtRequest({required this.index}); + + Map toMap() => { + 'index': index, + }; +} + +class SkipToNextRequest { + Map toMap() => {}; +} + +class SkipToPreviousRequest { + Map toMap() => {}; +} + +class FastForwardRequest { + Map toMap() => {}; +} + +class RewindRequest { + Map toMap() => {}; +} + +class SkipToQueueItemRequest { + final int index; + + SkipToQueueItemRequest({required this.index}); + + Map toMap() => { + 'index': index, + }; +} + +class SeekRequest { + final Duration position; + + SeekRequest({required this.position}); + + Map toMap() => { + 'position': position.inMicroseconds, + }; +} + +class SetRatingRequest { + final RatingMessage rating; + final Map? extras; + + SetRatingRequest({required this.rating, this.extras}); + + Map toMap() => { + 'rating': rating.toMap(), + 'extras': extras, + }; +} + +class SetCaptioningEnabledRequest { + final bool enabled; + + SetCaptioningEnabledRequest({required this.enabled}); + + Map toMap() => { + 'enabled': enabled, + }; +} + +class SetRepeatModeRequest { + final AudioServiceRepeatModeMessage repeatMode; + + SetRepeatModeRequest({required this.repeatMode}); + + Map toMap() => { + 'repeatMode': repeatMode.index, + }; +} + +class SetShuffleModeRequest { + final AudioServiceShuffleModeMessage shuffleMode; + + SetShuffleModeRequest({required this.shuffleMode}); + + Map toMap() => { + 'shuffleMode': shuffleMode.index, + }; +} + +class SeekBackwardRequest { + final bool begin; + + SeekBackwardRequest({required this.begin}); + + Map toMap() => { + 'begin': begin, + }; +} + +class SeekForwardRequest { + final bool begin; + + SeekForwardRequest({required this.begin}); + + Map toMap() => { + 'begin': begin, + }; +} + +class SetSpeedRequest { + final double speed; + + SetSpeedRequest({required this.speed}); + + Map toMap() => { + 'speed': speed, + }; +} + +class CustomActionRequest { + final String name; + final Map? extras; + + CustomActionRequest({required this.name, this.extras}); + + Map toMap() => { + 'name': name, + 'extras': extras, + }; +} + +class OnTaskRemovedRequest { + Map toMap() => {}; +} + +class OnNotificationDeletedRequest { + Map toMap() => {}; +} + +class GetChildrenRequest { + final String parentMediaId; + final Map? options; + + GetChildrenRequest({required this.parentMediaId, this.options}); + + Map toMap() => { + 'parentMediaId': parentMediaId, + 'options': options, + }; +} + +class GetChildrenResponse { + final List children; + + GetChildrenResponse({required this.children}); + + Map toMap() => { + 'children': children.map((item) => item.toMap()).toList(), + }; +} + +class GetMediaItemRequest { + final String mediaId; + + GetMediaItemRequest({required this.mediaId}); + + Map toMap() => { + 'mediaId': mediaId, + }; +} + +class GetMediaItemResponse { + final MediaItemMessage? mediaItem; + + GetMediaItemResponse({required this.mediaItem}); + + Map toMap() => { + 'mediaItem': mediaItem?.toMap(), + }; +} + +class SearchRequest { + final String query; + final Map? extras; + + SearchRequest({required this.query, this.extras}); + + Map toMap() => { + 'query': query, + 'extras': extras, + }; +} + +class SearchResponse { + final List mediaItems; + + SearchResponse({required this.mediaItems}); + + Map toMap() => { + 'mediaItems': mediaItems.map((item) => item.toMap()).toList(), + }; +} + +class AndroidSetRemoteVolumeRequest { + final int volumeIndex; + + AndroidSetRemoteVolumeRequest({required this.volumeIndex}); + + Map toMap() => { + 'volumeIndex': volumeIndex, + }; +} + +class AndroidAdjustRemoteVolumeRequest { + final AndroidVolumeDirectionMessage direction; + + AndroidAdjustRemoteVolumeRequest({required this.direction}); + + Map toMap() => { + 'direction': direction.index, + }; +} diff --git a/audio_service_platform_interface_entities/pubspec.yaml b/audio_service_platform_interface_entities/pubspec.yaml new file mode 100644 index 00000000..bd20729c --- /dev/null +++ b/audio_service_platform_interface_entities/pubspec.yaml @@ -0,0 +1,7 @@ +name: audio_service_platform_interface_entities +description: Data classes for audio_service_platform_interface. +version: 0.1.0 +homepage: https://github.com/ryanheise/audio_service/tree/master/audio_service_platform_interface_entities + +environment: + sdk: '>=2.12.0 <3.0.0'