audio_service.dart 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848
  1. import 'dart:async';
  2. import 'dart:io' show Platform;
  3. import 'dart:isolate';
  4. import 'dart:ui' as ui;
  5. import 'dart:ui';
  6. import 'package:audio_session/audio_session.dart';
  7. import 'package:flutter/foundation.dart';
  8. import 'package:flutter/material.dart';
  9. import 'package:flutter/services.dart';
  10. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  11. import 'package:flutter_isolate/flutter_isolate.dart';
  12. import 'package:rxdart/rxdart.dart';
  13. /// Name of port used to send custom events.
  14. const _CUSTOM_EVENT_PORT_NAME = 'customEventPort';
  15. /// The different buttons on a headset.
  16. enum MediaButton {
  17. media,
  18. next,
  19. previous,
  20. }
  21. /// The actons associated with playing audio.
  22. enum MediaAction {
  23. stop,
  24. pause,
  25. play,
  26. rewind,
  27. skipToPrevious,
  28. skipToNext,
  29. fastForward,
  30. setRating,
  31. seekTo,
  32. playPause,
  33. playFromMediaId,
  34. playFromSearch,
  35. skipToQueueItem,
  36. playFromUri,
  37. prepare,
  38. prepareFromMediaId,
  39. prepareFromSearch,
  40. prepareFromUri,
  41. setRepeatMode,
  42. unused_1,
  43. unused_2,
  44. setShuffleMode,
  45. seekBackward,
  46. seekForward,
  47. }
  48. /// The different states during audio processing.
  49. enum AudioProcessingState {
  50. none,
  51. connecting,
  52. ready,
  53. buffering,
  54. fastForwarding,
  55. rewinding,
  56. skippingToPrevious,
  57. skippingToNext,
  58. skippingToQueueItem,
  59. completed,
  60. stopped,
  61. error,
  62. }
  63. /// The playback state for the audio service which includes a [playing] boolean
  64. /// state, a processing state such as [AudioProcessingState.buffering], the
  65. /// playback position and the currently enabled actions to be shown in the
  66. /// Android notification or the iOS control center.
  67. class PlaybackState {
  68. /// The audio processing state e.g. [BasicPlaybackState.buffering].
  69. final AudioProcessingState processingState;
  70. /// Whether audio is either playing, or will play as soon as
  71. /// [processingState] is [AudioProcessingState.ready]. A true value should
  72. /// be broadcast whenever it would be appropriate for UIs to display a pause
  73. /// or stop button.
  74. ///
  75. /// Since [playing] and [processingState] can vary independently, it is
  76. /// possible distinguish a particular audio processing state while audio is
  77. /// playing vs paused. For example, when buffering occurs during a seek, the
  78. /// [processingState] can be [AudioProcessingState.buffering], but alongside
  79. /// that [playing] can be true to indicate that the seek was performed while
  80. /// playing, or false to indicate that the seek was performed while paused.
  81. final bool playing;
  82. /// The set of actions currently supported by the audio service e.g.
  83. /// [MediaAction.play].
  84. final Set<MediaAction> actions;
  85. /// The playback position at the last update time.
  86. final Duration position;
  87. /// The buffered position.
  88. final Duration bufferedPosition;
  89. /// The current playback speed where 1.0 means normal speed.
  90. final double speed;
  91. /// The time at which the playback position was last updated.
  92. final Duration updateTime;
  93. /// The current repeat mode.
  94. final AudioServiceRepeatMode repeatMode;
  95. /// The current shuffle mode.
  96. final AudioServiceShuffleMode shuffleMode;
  97. const PlaybackState({
  98. @required this.processingState,
  99. @required this.playing,
  100. @required this.actions,
  101. this.position,
  102. this.bufferedPosition = Duration.zero,
  103. this.speed,
  104. this.updateTime,
  105. this.repeatMode = AudioServiceRepeatMode.none,
  106. this.shuffleMode = AudioServiceShuffleMode.none,
  107. });
  108. /// The current playback position.
  109. Duration get currentPosition {
  110. if (playing && processingState == AudioProcessingState.ready) {
  111. return Duration(
  112. milliseconds: (position.inMilliseconds +
  113. ((DateTime.now().millisecondsSinceEpoch -
  114. updateTime.inMilliseconds) *
  115. (speed ?? 1.0)))
  116. .toInt());
  117. } else {
  118. return position;
  119. }
  120. }
  121. }
  122. enum RatingStyle {
  123. /// Indicates a rating style is not supported.
  124. ///
  125. /// A Rating will never have this type, but can be used by other classes
  126. /// to indicate they do not support Rating.
  127. none,
  128. /// A rating style with a single degree of rating, "heart" vs "no heart".
  129. ///
  130. /// Can be used to indicate the content referred to is a favorite (or not).
  131. heart,
  132. /// A rating style for "thumb up" vs "thumb down".
  133. thumbUpDown,
  134. /// A rating style with 0 to 3 stars.
  135. range3stars,
  136. /// A rating style with 0 to 4 stars.
  137. range4stars,
  138. /// A rating style with 0 to 5 stars.
  139. range5stars,
  140. /// A rating style expressed as a percentage.
  141. percentage,
  142. }
  143. /// A rating to attach to a MediaItem.
  144. class Rating {
  145. final RatingStyle _type;
  146. final dynamic _value;
  147. const Rating._internal(this._type, this._value);
  148. /// Create a new heart rating.
  149. const Rating.newHeartRating(bool hasHeart)
  150. : this._internal(RatingStyle.heart, hasHeart);
  151. /// Create a new percentage rating.
  152. factory Rating.newPercentageRating(double percent) {
  153. if (percent < 0 || percent > 100) throw ArgumentError();
  154. return Rating._internal(RatingStyle.percentage, percent);
  155. }
  156. /// Create a new star rating.
  157. factory Rating.newStartRating(RatingStyle starRatingStyle, int starRating) {
  158. if (starRatingStyle != RatingStyle.range3stars &&
  159. starRatingStyle != RatingStyle.range4stars &&
  160. starRatingStyle != RatingStyle.range5stars) {
  161. throw ArgumentError();
  162. }
  163. if (starRating > starRatingStyle.index || starRating < 0)
  164. throw ArgumentError();
  165. return Rating._internal(starRatingStyle, starRating);
  166. }
  167. /// Create a new thumb rating.
  168. const Rating.newThumbRating(bool isThumbsUp)
  169. : this._internal(RatingStyle.thumbUpDown, isThumbsUp);
  170. /// Create a new unrated rating.
  171. const Rating.newUnratedRating(RatingStyle ratingStyle)
  172. : this._internal(ratingStyle, null);
  173. /// Return the rating style.
  174. RatingStyle getRatingStyle() => _type;
  175. /// Returns a percentage rating value greater or equal to 0.0f, or a
  176. /// negative value if the rating style is not percentage-based, or
  177. /// if it is unrated.
  178. double getPercentRating() {
  179. if (_type != RatingStyle.percentage) return -1;
  180. if (_value < 0 || _value > 100) return -1;
  181. return _value ?? -1;
  182. }
  183. /// Returns a rating value greater or equal to 0.0f, or a negative
  184. /// value if the rating style is not star-based, or if it is
  185. /// unrated.
  186. int getStarRating() {
  187. if (_type != RatingStyle.range3stars &&
  188. _type != RatingStyle.range4stars &&
  189. _type != RatingStyle.range5stars) return -1;
  190. return _value ?? -1;
  191. }
  192. /// Returns true if the rating is "heart selected" or false if the
  193. /// rating is "heart unselected", if the rating style is not [heart]
  194. /// or if it is unrated.
  195. bool hasHeart() {
  196. if (_type != RatingStyle.heart) return false;
  197. return _value ?? false;
  198. }
  199. /// Returns true if the rating is "thumb up" or false if the rating
  200. /// is "thumb down", if the rating style is not [thumbUpDown] or if
  201. /// it is unrated.
  202. bool isThumbUp() {
  203. if (_type != RatingStyle.thumbUpDown) return false;
  204. return _value ?? false;
  205. }
  206. /// Return whether there is a rating value available.
  207. bool isRated() => _value != null;
  208. Map<String, dynamic> _toRaw() {
  209. return <String, dynamic>{
  210. 'type': _type.index,
  211. 'value': _value,
  212. };
  213. }
  214. // Even though this should take a Map<String, dynamic>, that makes an error.
  215. Rating._fromRaw(Map<dynamic, dynamic> raw)
  216. : this._internal(RatingStyle.values[raw['type']], raw['value']);
  217. }
  218. /// Metadata about an audio item that can be played, or a folder containing
  219. /// audio items.
  220. class MediaItem {
  221. /// A unique id.
  222. final String id;
  223. /// The album this media item belongs to.
  224. final String album;
  225. /// The title of this media item.
  226. final String title;
  227. /// The artist of this media item.
  228. final String artist;
  229. /// The genre of this media item.
  230. final String genre;
  231. /// The duration of this media item.
  232. final Duration duration;
  233. /// The artwork for this media item as a uri.
  234. final String artUri;
  235. /// Whether this is playable (i.e. not a folder).
  236. final bool playable;
  237. /// Override the default title for display purposes.
  238. final String displayTitle;
  239. /// Override the default subtitle for display purposes.
  240. final String displaySubtitle;
  241. /// Override the default description for display purposes.
  242. final String displayDescription;
  243. /// The rating of the MediaItem.
  244. final Rating rating;
  245. /// A map of additional metadata for the media item.
  246. ///
  247. /// The values must be integers or strings.
  248. final Map<String, dynamic> extras;
  249. /// Creates a [MediaItem].
  250. ///
  251. /// [id], [album] and [title] must not be null, and [id] must be unique for
  252. /// each instance.
  253. const MediaItem({
  254. @required this.id,
  255. @required this.album,
  256. @required this.title,
  257. this.artist,
  258. this.genre,
  259. this.duration,
  260. this.artUri,
  261. this.playable = true,
  262. this.displayTitle,
  263. this.displaySubtitle,
  264. this.displayDescription,
  265. this.rating,
  266. this.extras,
  267. });
  268. /// Creates a [MediaItem] from a map of key/value pairs corresponding to
  269. /// fields of this class.
  270. factory MediaItem.fromJson(Map raw) => MediaItem(
  271. id: raw['id'],
  272. album: raw['album'],
  273. title: raw['title'],
  274. artist: raw['artist'],
  275. genre: raw['genre'],
  276. duration: raw['duration'] != null
  277. ? Duration(milliseconds: raw['duration'])
  278. : null,
  279. artUri: raw['artUri'],
  280. playable: raw['playable'],
  281. displayTitle: raw['displayTitle'],
  282. displaySubtitle: raw['displaySubtitle'],
  283. displayDescription: raw['displayDescription'],
  284. rating: raw['rating'] != null ? Rating._fromRaw(raw['rating']) : null,
  285. extras: _raw2extras(raw['extras']),
  286. );
  287. /// Creates a copy of this [MediaItem] but with with the given fields
  288. /// replaced by new values.
  289. MediaItem copyWith({
  290. String id,
  291. String album,
  292. String title,
  293. String artist,
  294. String genre,
  295. Duration duration,
  296. String artUri,
  297. bool playable,
  298. String displayTitle,
  299. String displaySubtitle,
  300. String displayDescription,
  301. Rating rating,
  302. Map extras,
  303. }) =>
  304. MediaItem(
  305. id: id ?? this.id,
  306. album: album ?? this.album,
  307. title: title ?? this.title,
  308. artist: artist ?? this.artist,
  309. genre: genre ?? this.genre,
  310. duration: duration ?? this.duration,
  311. artUri: artUri ?? this.artUri,
  312. playable: playable ?? this.playable,
  313. displayTitle: displayTitle ?? this.displayTitle,
  314. displaySubtitle: displaySubtitle ?? this.displaySubtitle,
  315. displayDescription: displayDescription ?? this.displayDescription,
  316. rating: rating ?? this.rating,
  317. extras: extras ?? this.extras,
  318. );
  319. @override
  320. int get hashCode => id.hashCode;
  321. @override
  322. bool operator ==(dynamic other) => other is MediaItem && other.id == id;
  323. @override
  324. String toString() => '${toJson()}';
  325. /// Converts this [MediaItem] to a map of key/value pairs corresponding to
  326. /// the fields of this class.
  327. Map<String, dynamic> toJson() => {
  328. 'id': id,
  329. 'album': album,
  330. 'title': title,
  331. 'artist': artist,
  332. 'genre': genre,
  333. 'duration': duration?.inMilliseconds,
  334. 'artUri': artUri,
  335. 'playable': playable,
  336. 'displayTitle': displayTitle,
  337. 'displaySubtitle': displaySubtitle,
  338. 'displayDescription': displayDescription,
  339. 'rating': rating?._toRaw(),
  340. 'extras': extras,
  341. };
  342. static Map<String, dynamic> _raw2extras(Map raw) {
  343. if (raw == null) return null;
  344. final extras = <String, dynamic>{};
  345. for (var key in raw.keys) {
  346. extras[key as String] = raw[key];
  347. }
  348. return extras;
  349. }
  350. }
  351. /// A button to appear in the Android notification, lock screen, Android smart
  352. /// watch, or Android Auto device. The set of buttons you would like to display
  353. /// at any given moment should be set via [AudioServiceBackground.setState].
  354. ///
  355. /// Each [MediaControl] button controls a specified [MediaAction]. Only the
  356. /// following actions can be represented as buttons:
  357. ///
  358. /// * [MediaAction.stop]
  359. /// * [MediaAction.pause]
  360. /// * [MediaAction.play]
  361. /// * [MediaAction.rewind]
  362. /// * [MediaAction.skipToPrevious]
  363. /// * [MediaAction.skipToNext]
  364. /// * [MediaAction.fastForward]
  365. /// * [MediaAction.playPause]
  366. ///
  367. /// Predefined controls with default Android icons and labels are defined as
  368. /// static fields of this class. If you wish to define your own custom Android
  369. /// controls with your own icon resources, you will need to place the Android
  370. /// resources in `android/app/src/main/res`. Here, you will find a subdirectory
  371. /// for each different resolution:
  372. ///
  373. /// ```
  374. /// drawable-hdpi
  375. /// drawable-mdpi
  376. /// drawable-xhdpi
  377. /// drawable-xxhdpi
  378. /// drawable-xxxhdpi
  379. /// ```
  380. ///
  381. /// You can use [Android Asset
  382. /// Studio](https://romannurik.github.io/AndroidAssetStudio/) to generate these
  383. /// different subdirectories for any standard material design icon.
  384. class MediaControl {
  385. /// A default control for [MediaAction.stop].
  386. static final stop = MediaControl(
  387. androidIcon: 'drawable/audio_service_stop',
  388. label: 'Stop',
  389. action: MediaAction.stop,
  390. );
  391. /// A default control for [MediaAction.pause].
  392. static final pause = MediaControl(
  393. androidIcon: 'drawable/audio_service_pause',
  394. label: 'Pause',
  395. action: MediaAction.pause,
  396. );
  397. /// A default control for [MediaAction.play].
  398. static final play = MediaControl(
  399. androidIcon: 'drawable/audio_service_play_arrow',
  400. label: 'Play',
  401. action: MediaAction.play,
  402. );
  403. /// A default control for [MediaAction.rewind].
  404. static final rewind = MediaControl(
  405. androidIcon: 'drawable/audio_service_fast_rewind',
  406. label: 'Rewind',
  407. action: MediaAction.rewind,
  408. );
  409. /// A default control for [MediaAction.skipToNext].
  410. static final skipToNext = MediaControl(
  411. androidIcon: 'drawable/audio_service_skip_next',
  412. label: 'Next',
  413. action: MediaAction.skipToNext,
  414. );
  415. /// A default control for [MediaAction.skipToPrevious].
  416. static final skipToPrevious = MediaControl(
  417. androidIcon: 'drawable/audio_service_skip_previous',
  418. label: 'Previous',
  419. action: MediaAction.skipToPrevious,
  420. );
  421. /// A default control for [MediaAction.fastForward].
  422. static final fastForward = MediaControl(
  423. androidIcon: 'drawable/audio_service_fast_forward',
  424. label: 'Fast Forward',
  425. action: MediaAction.fastForward,
  426. );
  427. /// A reference to an Android icon resource for the control (e.g.
  428. /// `"drawable/ic_action_pause"`)
  429. final String androidIcon;
  430. /// A label for the control
  431. final String label;
  432. /// The action to be executed by this control
  433. final MediaAction action;
  434. const MediaControl({
  435. @required this.androidIcon,
  436. @required this.label,
  437. @required this.action,
  438. });
  439. }
  440. const MethodChannel _channel =
  441. const MethodChannel('ryanheise.com/audioService');
  442. const String _CUSTOM_PREFIX = 'custom_';
  443. /// Client API to start and interact with the audio service.
  444. ///
  445. /// This class is used from your UI code to establish a connection with the
  446. /// audio service. While connected to the service, your UI may invoke methods
  447. /// of this class to start/pause/stop/etc. playback and listen to changes in
  448. /// playback state and playing media.
  449. ///
  450. /// Your UI must disconnect from the audio service when it is no longer visible
  451. /// although the audio service will continue to run in the background. If your
  452. /// UI once again becomes visible, you should reconnect to the audio service.
  453. ///
  454. /// It is recommended to use [AudioServiceWidget] to manage this connection
  455. /// automatically.
  456. class AudioService {
  457. /// True if the background task runs in its own isolate, false if it doesn't.
  458. static bool get usesIsolate => !(kIsWeb || Platform.isMacOS);
  459. /// The root media ID for browsing media provided by the background
  460. /// task.
  461. static const String MEDIA_ROOT_ID = "root";
  462. static final _browseMediaChildrenSubject = BehaviorSubject<List<MediaItem>>();
  463. /// A stream that broadcasts the children of the current browse
  464. /// media parent.
  465. static Stream<List<MediaItem>> get browseMediaChildrenStream =>
  466. _browseMediaChildrenSubject.stream;
  467. static final _playbackStateSubject = BehaviorSubject<PlaybackState>();
  468. /// A stream that broadcasts the playback state.
  469. static Stream<PlaybackState> get playbackStateStream =>
  470. _playbackStateSubject.stream;
  471. static final _currentMediaItemSubject = BehaviorSubject<MediaItem>();
  472. /// A stream that broadcasts the current [MediaItem].
  473. static Stream<MediaItem> get currentMediaItemStream =>
  474. _currentMediaItemSubject.stream;
  475. static final _queueSubject = BehaviorSubject<List<MediaItem>>();
  476. /// A stream that broadcasts the queue.
  477. static Stream<List<MediaItem>> get queueStream => _queueSubject.stream;
  478. static final _notificationSubject = BehaviorSubject<bool>.seeded(false);
  479. /// A stream that broadcasts the status of notificationClick event.
  480. static Stream<bool> get notificationClickEventStream =>
  481. _notificationSubject.stream;
  482. static final _customEventSubject = PublishSubject<dynamic>();
  483. /// A stream that broadcasts custom events sent from the background.
  484. static Stream<dynamic> get customEventStream => _customEventSubject.stream;
  485. /// The children of the current browse media parent.
  486. static List<MediaItem> get browseMediaChildren => _browseMediaChildren;
  487. static List<MediaItem> _browseMediaChildren;
  488. /// The current playback state.
  489. static PlaybackState get playbackState => _playbackState;
  490. static PlaybackState _playbackState;
  491. /// The current media item.
  492. static MediaItem get currentMediaItem => _currentMediaItem;
  493. static MediaItem _currentMediaItem;
  494. /// The current queue.
  495. static List<MediaItem> get queue => _queue;
  496. static List<MediaItem> _queue;
  497. /// True after service stopped and !running.
  498. static bool _afterStop = false;
  499. /// Receives custom events from the background audio task.
  500. static ReceivePort _customEventReceivePort;
  501. static StreamSubscription _customEventSubscription;
  502. /// A queue of tasks to be processed serially. Tasks that are processed on
  503. /// this queue:
  504. ///
  505. /// - [connect]
  506. /// - [disconnect]
  507. /// - [start]
  508. ///
  509. /// TODO: Queue other tasks? Note, only short-running tasks should be queued.
  510. static final _asyncTaskQueue = _AsyncTaskQueue();
  511. /// Connects to the service from your UI so that audio playback can be
  512. /// controlled.
  513. ///
  514. /// This method should be called when your UI becomes visible, and
  515. /// [disconnect] should be called when your UI is no longer visible. All
  516. /// other methods in this class will work only while connected.
  517. ///
  518. /// Use [AudioServiceWidget] to handle this automatically.
  519. static Future<void> connect() => _asyncTaskQueue.schedule(() async {
  520. if (_connected) return;
  521. _channel.setMethodCallHandler((MethodCall call) async {
  522. switch (call.method) {
  523. case 'onChildrenLoaded':
  524. final List<Map> args = List<Map>.from(call.arguments[0]);
  525. _browseMediaChildren =
  526. args.map((raw) => MediaItem.fromJson(raw)).toList();
  527. _browseMediaChildrenSubject.add(_browseMediaChildren);
  528. break;
  529. case 'onPlaybackStateChanged':
  530. // If this event arrives too late, ignore it.
  531. if (_afterStop) return;
  532. final List args = call.arguments;
  533. int actionBits = args[2];
  534. _playbackState = PlaybackState(
  535. processingState: AudioProcessingState.values[args[0]],
  536. playing: args[1],
  537. actions: MediaAction.values
  538. .where((action) => (actionBits & (1 << action.index)) != 0)
  539. .toSet(),
  540. position: Duration(milliseconds: args[3]),
  541. bufferedPosition: Duration(milliseconds: args[4]),
  542. speed: args[5],
  543. updateTime: Duration(milliseconds: args[6]),
  544. repeatMode: AudioServiceRepeatMode.values[args[7]],
  545. shuffleMode: AudioServiceShuffleMode.values[args[8]],
  546. );
  547. _playbackStateSubject.add(_playbackState);
  548. break;
  549. case 'onMediaChanged':
  550. _currentMediaItem = call.arguments[0] != null
  551. ? MediaItem.fromJson(call.arguments[0])
  552. : null;
  553. _currentMediaItemSubject.add(_currentMediaItem);
  554. break;
  555. case 'onQueueChanged':
  556. final List<Map> args = call.arguments[0] != null
  557. ? List<Map>.from(call.arguments[0])
  558. : null;
  559. _queue = args?.map((raw) => MediaItem.fromJson(raw))?.toList();
  560. _queueSubject.add(_queue);
  561. break;
  562. case 'onStopped':
  563. _browseMediaChildren = null;
  564. _browseMediaChildrenSubject.add(null);
  565. _playbackState = null;
  566. _playbackStateSubject.add(null);
  567. _currentMediaItem = null;
  568. _currentMediaItemSubject.add(null);
  569. _queue = null;
  570. _queueSubject.add(null);
  571. _notificationSubject.add(false);
  572. _running = false;
  573. _afterStop = true;
  574. break;
  575. case 'notificationClicked':
  576. _notificationSubject.add(call.arguments[0]);
  577. break;
  578. }
  579. });
  580. if (AudioService.usesIsolate) {
  581. _customEventReceivePort = ReceivePort();
  582. _customEventSubscription = _customEventReceivePort.listen((event) {
  583. _customEventSubject.add(event);
  584. });
  585. IsolateNameServer.removePortNameMapping(_CUSTOM_EVENT_PORT_NAME);
  586. IsolateNameServer.registerPortWithName(
  587. _customEventReceivePort.sendPort, _CUSTOM_EVENT_PORT_NAME);
  588. }
  589. await _channel.invokeMethod("connect");
  590. _running = await _channel.invokeMethod("isRunning");
  591. _connected = true;
  592. });
  593. /// Disconnects your UI from the service.
  594. ///
  595. /// This method should be called when the UI is no longer visible.
  596. ///
  597. /// Use [AudioServiceWidget] to handle this automatically.
  598. static Future<void> disconnect() => _asyncTaskQueue.schedule(() async {
  599. if (!_connected) return;
  600. _channel.setMethodCallHandler(null);
  601. _customEventSubscription?.cancel();
  602. _customEventSubscription = null;
  603. _customEventReceivePort = null;
  604. await _channel.invokeMethod("disconnect");
  605. _connected = false;
  606. });
  607. /// True if the UI is connected.
  608. static bool get connected => _connected;
  609. static bool _connected = false;
  610. /// True if the background audio task is running.
  611. static bool get running => _running;
  612. static bool _running = false;
  613. /// Starts a background audio task which will continue running even when the
  614. /// UI is not visible or the screen is turned off. Only one background audio task
  615. /// may be running at a time.
  616. ///
  617. /// While the background task is running, it will display a system
  618. /// notification showing information about the current media item being
  619. /// played (see [AudioServiceBackground.setMediaItem]) along with any media
  620. /// controls to perform any media actions that you want to support (see
  621. /// [AudioServiceBackground.setState]).
  622. ///
  623. /// The background task is specified by [backgroundTaskEntrypoint] which will
  624. /// be run within a background isolate. This function must be a top-level
  625. /// function, and it must initiate execution by calling
  626. /// [AudioServiceBackground.run]. Because the background task runs in an
  627. /// isolate, no memory is shared between the background isolate and your main
  628. /// UI isolate and so all communication between the background task and your
  629. /// UI is achieved through message passing.
  630. ///
  631. /// The [androidNotificationIcon] is specified like an XML resource reference
  632. /// and defaults to `"mipmap/ic_launcher"`.
  633. ///
  634. /// If specified, [androidArtDownscaleSize] causes artwork to be downscaled
  635. /// to the given resolution in pixels before being displayed in the
  636. /// notification and lock screen. If not specified, no downscaling will be
  637. /// performed. If the resolution of your artwork is particularly high,
  638. /// downscaling can help to conserve memory.
  639. ///
  640. /// [params] provides a way to pass custom parameters through to the
  641. /// `onStart` method of your background audio task. If specified, this must
  642. /// be a map consisting of keys/values that can be encoded via Flutter's
  643. /// `StandardMessageCodec`.
  644. ///
  645. /// [fastForwardInterval] and [rewindInterval] are passed through to your
  646. /// background audio task as properties, and they represent the duration
  647. /// of audio that should be skipped in fast forward / rewind operations. On
  648. /// iOS, these values also configure the intervals for the skip forward and
  649. /// skip backward buttons.
  650. ///
  651. /// [androidEnableQueue] enables queue support on the media session on
  652. /// Android. If your app will run on Android and has a queue, you should set
  653. /// this to true.
  654. ///
  655. /// [androidStopForegroundOnPause] will switch the Android service to a lower
  656. /// priority state when playback is paused allowing the user to swipe away the
  657. /// notification. Note that while in this lower priority state, the operating
  658. /// system will also be able to kill your service at any time to reclaim
  659. /// resources.
  660. ///
  661. /// This method waits for [BackgroundAudioTask.onStart] to complete, and
  662. /// completes with true if the task was successfully started, or false
  663. /// otherwise.
  664. static Future<bool> start({
  665. @required Function backgroundTaskEntrypoint,
  666. Map<String, dynamic> params,
  667. String androidNotificationChannelName = "Notifications",
  668. String androidNotificationChannelDescription,
  669. int androidNotificationColor,
  670. String androidNotificationIcon = 'mipmap/ic_launcher',
  671. bool androidNotificationClickStartsActivity = true,
  672. bool androidNotificationOngoing = false,
  673. bool androidResumeOnClick = true,
  674. bool androidStopForegroundOnPause = false,
  675. bool androidEnableQueue = false,
  676. Size androidArtDownscaleSize,
  677. Duration fastForwardInterval = const Duration(seconds: 10),
  678. Duration rewindInterval = const Duration(seconds: 10),
  679. }) async {
  680. return await _asyncTaskQueue.schedule(() async {
  681. if (!_connected) throw Exception("Not connected");
  682. if (_running) return false;
  683. _running = true;
  684. _afterStop = false;
  685. ui.CallbackHandle handle;
  686. if (AudioService.usesIsolate) {
  687. handle = ui.PluginUtilities.getCallbackHandle(backgroundTaskEntrypoint);
  688. if (handle == null) {
  689. return false;
  690. }
  691. }
  692. var callbackHandle = handle?.toRawHandle();
  693. if (kIsWeb) {
  694. // Platform throws runtime exceptions on web
  695. } else if (Platform.isIOS) {
  696. // NOTE: to maintain compatibility between the Android and iOS
  697. // implementations, we ensure that the iOS background task also runs in
  698. // an isolate. Currently, the standard Isolate API does not allow
  699. // isolates to invoke methods on method channels. That may be fixed in
  700. // the future, but until then, we use the flutter_isolate plugin which
  701. // creates a FlutterNativeView for us, similar to what the Android
  702. // implementation does.
  703. // TODO: remove dependency on flutter_isolate by either using the
  704. // FlutterNativeView API directly or by waiting until Flutter allows
  705. // regular isolates to use method channels.
  706. await FlutterIsolate.spawn(_iosIsolateEntrypoint, callbackHandle);
  707. }
  708. final success = await _channel.invokeMethod('start', {
  709. 'callbackHandle': callbackHandle,
  710. 'params': params,
  711. 'androidNotificationChannelName': androidNotificationChannelName,
  712. 'androidNotificationChannelDescription':
  713. androidNotificationChannelDescription,
  714. 'androidNotificationColor': androidNotificationColor,
  715. 'androidNotificationIcon': androidNotificationIcon,
  716. 'androidNotificationClickStartsActivity':
  717. androidNotificationClickStartsActivity,
  718. 'androidNotificationOngoing': androidNotificationOngoing,
  719. 'androidResumeOnClick': androidResumeOnClick,
  720. 'androidStopForegroundOnPause': androidStopForegroundOnPause,
  721. 'androidEnableQueue': androidEnableQueue,
  722. 'androidArtDownscaleSize': androidArtDownscaleSize != null
  723. ? {
  724. 'width': androidArtDownscaleSize.width,
  725. 'height': androidArtDownscaleSize.height
  726. }
  727. : null,
  728. 'fastForwardInterval': fastForwardInterval.inMilliseconds,
  729. 'rewindInterval': rewindInterval.inMilliseconds,
  730. });
  731. _running = await _channel.invokeMethod("isRunning");
  732. if (!AudioService.usesIsolate) backgroundTaskEntrypoint();
  733. return success;
  734. });
  735. }
  736. /// Sets the parent of the children that [browseMediaChildrenStream] broadcasts.
  737. /// If unspecified, the root parent will be used.
  738. static Future<void> setBrowseMediaParent(
  739. [String parentMediaId = MEDIA_ROOT_ID]) async {
  740. await _channel.invokeMethod('setBrowseMediaParent', parentMediaId);
  741. }
  742. /// Sends a request to your background audio task to add an item to the
  743. /// queue. This passes through to the `onAddQueueItem` method in your
  744. /// background audio task.
  745. static Future<void> addQueueItem(MediaItem mediaItem) async {
  746. await _channel.invokeMethod('addQueueItem', mediaItem.toJson());
  747. }
  748. /// Sends a request to your background audio task to add a item to the queue
  749. /// at a particular position. This passes through to the `onAddQueueItemAt`
  750. /// method in your background audio task.
  751. static Future<void> addQueueItemAt(MediaItem mediaItem, int index) async {
  752. await _channel.invokeMethod('addQueueItemAt', [mediaItem.toJson(), index]);
  753. }
  754. /// Sends a request to your background audio task to remove an item from the
  755. /// queue. This passes through to the `onRemoveQueueItem` method in your
  756. /// background audio task.
  757. static Future<void> removeQueueItem(MediaItem mediaItem) async {
  758. await _channel.invokeMethod('removeQueueItem', mediaItem.toJson());
  759. }
  760. /// A convenience method calls [addQueueItem] for each media item in the
  761. /// given list. Note that this will be inefficient if you are adding a lot
  762. /// of media items at once. If possible, you should use [updateQueue]
  763. /// instead.
  764. static Future<void> addQueueItems(List<MediaItem> mediaItems) async {
  765. for (var mediaItem in mediaItems) {
  766. await addQueueItem(mediaItem);
  767. }
  768. }
  769. /// Sends a request to your background audio task to replace the queue with a
  770. /// new list of media items. This passes through to the `onUpdateQueue`
  771. /// method in your background audio task.
  772. static Future<void> updateQueue(List<MediaItem> queue) async {
  773. await _channel.invokeMethod(
  774. 'updateQueue', queue.map((item) => item.toJson()).toList());
  775. }
  776. /// Sends a request to your background audio task to update the details of a
  777. /// media item. This passes through to the 'onUpdateMediaItem' method in your
  778. /// background audio task.
  779. static Future<void> updateMediaItem(MediaItem mediaItem) async {
  780. await _channel.invokeMethod('updateMediaItem', mediaItem.toJson());
  781. }
  782. /// Programmatically simulates a click of a media button on the headset.
  783. ///
  784. /// This passes through to `onClick` in the background audio task.
  785. static Future<void> click([MediaButton button = MediaButton.media]) async {
  786. await _channel.invokeMethod('click', button.index);
  787. }
  788. /// Sends a request to your background audio task to prepare for audio
  789. /// playback. This passes through to the `onPrepare` method in your
  790. /// background audio task.
  791. static Future<void> prepare() async {
  792. await _channel.invokeMethod('prepare');
  793. }
  794. /// Sends a request to your background audio task to prepare for playing a
  795. /// particular media item. This passes through to the `onPrepareFromMediaId`
  796. /// method in your background audio task.
  797. static Future<void> prepareFromMediaId(String mediaId) async {
  798. await _channel.invokeMethod('prepareFromMediaId', mediaId);
  799. }
  800. //static Future<void> prepareFromSearch(String query, Bundle extras) async {}
  801. //static Future<void> prepareFromUri(Uri uri, Bundle extras) async {}
  802. /// Sends a request to your background audio task to play the current media
  803. /// item. This passes through to 'onPlay' in your background audio task.
  804. static Future<void> play() async {
  805. await _channel.invokeMethod('play');
  806. }
  807. /// Sends a request to your background audio task to play a particular media
  808. /// item referenced by its media id. This passes through to the
  809. /// 'onPlayFromMediaId' method in your background audio task.
  810. static Future<void> playFromMediaId(String mediaId) async {
  811. await _channel.invokeMethod('playFromMediaId', mediaId);
  812. }
  813. /// Sends a request to your background audio task to play a particular media
  814. /// item. This passes through to the 'onPlayMediaItem' method in your
  815. /// background audio task.
  816. static Future<void> playMediaItem(MediaItem mediaItem) async {
  817. await _channel.invokeMethod('playMediaItem', mediaItem.toJson());
  818. }
  819. //static Future<void> playFromSearch(String query, Bundle extras) async {}
  820. //static Future<void> playFromUri(Uri uri, Bundle extras) async {}
  821. /// Sends a request to your background audio task to skip to a particular
  822. /// item in the queue. This passes through to the `onSkipToQueueItem` method
  823. /// in your background audio task.
  824. static Future<void> skipToQueueItem(String mediaId) async {
  825. await _channel.invokeMethod('skipToQueueItem', mediaId);
  826. }
  827. /// Sends a request to your background audio task to pause playback. This
  828. /// passes through to the `onPause` method in your background audio task.
  829. static Future<void> pause() async {
  830. await _channel.invokeMethod('pause');
  831. }
  832. /// Sends a request to your background audio task to stop playback and shut
  833. /// down the task. This passes through to the `onStop` method in your
  834. /// background audio task.
  835. static Future<void> stop() async {
  836. await _channel.invokeMethod('stop');
  837. }
  838. /// Sends a request to your background audio task to seek to a particular
  839. /// position in the current media item. This passes through to the `onSeekTo`
  840. /// method in your background audio task.
  841. static Future<void> seekTo(Duration position) async {
  842. await _channel.invokeMethod('seekTo', position.inMilliseconds);
  843. }
  844. /// Sends a request to your background audio task to skip to the next item in
  845. /// the queue. This passes through to the `onSkipToNext` method in your
  846. /// background audio task.
  847. static Future<void> skipToNext() async {
  848. await _channel.invokeMethod('skipToNext');
  849. }
  850. /// Sends a request to your background audio task to skip to the previous
  851. /// item in the queue. This passes through to the `onSkipToPrevious` method
  852. /// in your background audio task.
  853. static Future<void> skipToPrevious() async {
  854. await _channel.invokeMethod('skipToPrevious');
  855. }
  856. /// Sends a request to your background audio task to fast forward by the
  857. /// interval passed into the [start] method. This passes through to the
  858. /// `onFastForward` method in your background audio task.
  859. static Future<void> fastForward() async {
  860. await _channel.invokeMethod('fastForward');
  861. }
  862. /// Sends a request to your background audio task to rewind by the interval
  863. /// passed into the [start] method. This passes through to the `onRewind`
  864. /// method in the background audio task.
  865. static Future<void> rewind() async {
  866. await _channel.invokeMethod('rewind');
  867. }
  868. //static Future<void> setCaptioningEnabled(boolean enabled) async {}
  869. /// Sends a request to your background audio task to set the repeat mode.
  870. /// This passes through to the `onSetRepeatMode` method in your background
  871. /// audio task.
  872. static Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
  873. await _channel.invokeMethod('setRepeatMode', repeatMode.index);
  874. }
  875. /// Sends a request to your background audio task to set the shuffle mode.
  876. /// This passes through to the `onSetShuffleMode` method in your background
  877. /// audio task.
  878. static Future<void> setShuffleMode(
  879. AudioServiceShuffleMode shuffleMode) async {
  880. await _channel.invokeMethod('setShuffleMode', shuffleMode.index);
  881. }
  882. /// Sends a request to your background audio task to set a rating on the
  883. /// current media item. This passes through to the `onSetRating` method in
  884. /// your background audio task. The extras map must *only* contain primitive
  885. /// types!
  886. static Future<void> setRating(Rating rating,
  887. [Map<String, dynamic> extras]) async {
  888. await _channel.invokeMethod('setRating', {
  889. "rating": rating._toRaw(),
  890. "extras": extras,
  891. });
  892. }
  893. /// Sends a request to your background audio task to set the audio playback
  894. /// speed. This passes through to the `onSetSpeed` method in your background
  895. /// audio task.
  896. static Future<void> setSpeed(double speed) async {
  897. await _channel.invokeMethod('setSpeed', speed);
  898. }
  899. /// Sends a request to your background audio task to begin or end seeking
  900. /// backward. This method passes through to the `onSeekBackward` method in
  901. /// your background audio task.
  902. static Future<void> seekBackward(bool begin) async {
  903. await _channel.invokeMethod('seekBackward', begin);
  904. }
  905. /// Sends a request to your background audio task to begin or end seek
  906. /// forward. This method passes through to the `onSeekForward` method in your
  907. /// background audio task.
  908. static Future<void> seekForward(bool begin) async {
  909. await _channel.invokeMethod('seekForward', begin);
  910. }
  911. //static Future<void> sendCustomAction(PlaybackStateCompat.CustomAction customAction,
  912. //static Future<void> sendCustomAction(String action, Bundle args) async {}
  913. /// Sends a custom request to your background audio task. This passes through
  914. /// to the `onCustomAction` in your background audio task.
  915. ///
  916. /// This may be used for your own purposes. [arguments] can be any data that
  917. /// is encodable by `StandardMessageCodec`.
  918. static Future customAction(String name, [dynamic arguments]) async {
  919. return await _channel.invokeMethod('$_CUSTOM_PREFIX$name', arguments);
  920. }
  921. }
  922. /// Background API to be used by your background audio task.
  923. ///
  924. /// The entry point of your background task that you passed to
  925. /// [AudioService.start] is executed in an isolate that will run independently
  926. /// of the view. Aside from its primary job of playing audio, your background
  927. /// task should also use methods of this class to initialise the isolate,
  928. /// broadcast state changes to any UI that may be connected, and to also handle
  929. /// playback actions initiated by the UI.
  930. class AudioServiceBackground {
  931. static final PlaybackState _noneState = PlaybackState(
  932. processingState: AudioProcessingState.none,
  933. playing: false,
  934. actions: Set(),
  935. );
  936. static MethodChannel _backgroundChannel;
  937. static PlaybackState _state = _noneState;
  938. static MediaItem _mediaItem;
  939. static List<MediaItem> _queue;
  940. static BaseCacheManager _cacheManager;
  941. static BackgroundAudioTask _task;
  942. static bool _running = false;
  943. /// The current media playback state.
  944. ///
  945. /// This is the value most recently set via [setState].
  946. static PlaybackState get state => _state;
  947. /// The current media item.
  948. ///
  949. /// This is the value most recently set via [setMediaItem].
  950. static MediaItem get mediaItem => _mediaItem;
  951. /// The current queue.
  952. ///
  953. /// This is the value most recently set via [setQueue].
  954. static List<MediaItem> get queue => _queue;
  955. /// Runs the background audio task within the background isolate.
  956. ///
  957. /// This must be the first method called by the entrypoint of your background
  958. /// task that you passed into [AudioService.start]. The [BackgroundAudioTask]
  959. /// returned by the [taskBuilder] parameter defines callbacks to handle the
  960. /// initialization and distruction of the background audio task, as well as
  961. /// any requests by the client to play, pause and otherwise control audio
  962. /// playback.
  963. static Future<void> run(BackgroundAudioTask taskBuilder()) async {
  964. _running = true;
  965. _backgroundChannel =
  966. const MethodChannel('ryanheise.com/audioServiceBackground');
  967. WidgetsFlutterBinding.ensureInitialized();
  968. _task = taskBuilder();
  969. _cacheManager = _task.cacheManager;
  970. _backgroundChannel.setMethodCallHandler((MethodCall call) async {
  971. try {
  972. switch (call.method) {
  973. case 'onLoadChildren':
  974. final List args = call.arguments;
  975. String parentMediaId = args[0];
  976. List<MediaItem> mediaItems =
  977. await _task.onLoadChildren(parentMediaId);
  978. List<Map> rawMediaItems =
  979. mediaItems.map((item) => item.toJson()).toList();
  980. return rawMediaItems as dynamic;
  981. case 'onClick':
  982. final List args = call.arguments;
  983. MediaButton button = MediaButton.values[args[0]];
  984. await _task.onClick(button);
  985. break;
  986. case 'onStop':
  987. await _task.onStop();
  988. break;
  989. case 'onPause':
  990. await _task.onPause();
  991. break;
  992. case 'onPrepare':
  993. await _task.onPrepare();
  994. break;
  995. case 'onPrepareFromMediaId':
  996. final List args = call.arguments;
  997. String mediaId = args[0];
  998. await _task.onPrepareFromMediaId(mediaId);
  999. break;
  1000. case 'onPlay':
  1001. await _task.onPlay();
  1002. break;
  1003. case 'onPlayFromMediaId':
  1004. final List args = call.arguments;
  1005. String mediaId = args[0];
  1006. await _task.onPlayFromMediaId(mediaId);
  1007. break;
  1008. case 'onPlayMediaItem':
  1009. await _task.onPlayMediaItem(MediaItem.fromJson(call.arguments[0]));
  1010. break;
  1011. case 'onAddQueueItem':
  1012. await _task.onAddQueueItem(MediaItem.fromJson(call.arguments[0]));
  1013. break;
  1014. case 'onAddQueueItemAt':
  1015. final List args = call.arguments;
  1016. MediaItem mediaItem = MediaItem.fromJson(args[0]);
  1017. int index = args[1];
  1018. await _task.onAddQueueItemAt(mediaItem, index);
  1019. break;
  1020. case 'onUpdateQueue':
  1021. final List args = call.arguments;
  1022. final List queue = args[0];
  1023. await _task.onUpdateQueue(
  1024. queue?.map((raw) => MediaItem.fromJson(raw))?.toList());
  1025. break;
  1026. case 'onUpdateMediaItem':
  1027. await _task
  1028. .onUpdateMediaItem(MediaItem.fromJson(call.arguments[0]));
  1029. break;
  1030. case 'onRemoveQueueItem':
  1031. await _task
  1032. .onRemoveQueueItem(MediaItem.fromJson(call.arguments[0]));
  1033. break;
  1034. case 'onSkipToNext':
  1035. await _task.onSkipToNext();
  1036. break;
  1037. case 'onSkipToPrevious':
  1038. await _task.onSkipToPrevious();
  1039. break;
  1040. case 'onFastForward':
  1041. await _task.onFastForward();
  1042. break;
  1043. case 'onRewind':
  1044. await _task.onRewind();
  1045. break;
  1046. case 'onSkipToQueueItem':
  1047. final List args = call.arguments;
  1048. String mediaId = args[0];
  1049. await _task.onSkipToQueueItem(mediaId);
  1050. break;
  1051. case 'onSeekTo':
  1052. final List args = call.arguments;
  1053. int positionMs = args[0];
  1054. Duration position = Duration(milliseconds: positionMs);
  1055. await _task.onSeekTo(position);
  1056. break;
  1057. case 'onSetRepeatMode':
  1058. final List args = call.arguments;
  1059. await _task.onSetRepeatMode(AudioServiceRepeatMode.values[args[0]]);
  1060. break;
  1061. case 'onSetShuffleMode':
  1062. final List args = call.arguments;
  1063. await _task
  1064. .onSetShuffleMode(AudioServiceShuffleMode.values[args[0]]);
  1065. break;
  1066. case 'onSetRating':
  1067. await _task.onSetRating(
  1068. Rating._fromRaw(call.arguments[0]), call.arguments[1]);
  1069. break;
  1070. case 'onSeekBackward':
  1071. final List args = call.arguments;
  1072. await _task.onSeekBackward(args[0]);
  1073. break;
  1074. case 'onSeekForward':
  1075. final List args = call.arguments;
  1076. await _task.onSeekForward(args[0]);
  1077. break;
  1078. case 'onSetSpeed':
  1079. final List args = call.arguments;
  1080. double speed = args[0];
  1081. await _task.onSetSpeed(speed);
  1082. break;
  1083. case 'onTaskRemoved':
  1084. await _task.onTaskRemoved();
  1085. break;
  1086. case 'onClose':
  1087. await _task.onClose();
  1088. break;
  1089. default:
  1090. if (call.method.startsWith(_CUSTOM_PREFIX)) {
  1091. final result = await _task.onCustomAction(
  1092. call.method.substring(_CUSTOM_PREFIX.length), call.arguments);
  1093. return result;
  1094. }
  1095. break;
  1096. }
  1097. } catch (e, stacktrace) {
  1098. print('$stacktrace');
  1099. throw PlatformException(code: '$e');
  1100. }
  1101. });
  1102. Map startParams = await _backgroundChannel.invokeMethod('ready');
  1103. Duration fastForwardInterval =
  1104. Duration(milliseconds: startParams['fastForwardInterval']);
  1105. Duration rewindInterval =
  1106. Duration(milliseconds: startParams['rewindInterval']);
  1107. Map<String, dynamic> params =
  1108. startParams['params']?.cast<String, dynamic>();
  1109. _task._setParams(
  1110. fastForwardInterval: fastForwardInterval,
  1111. rewindInterval: rewindInterval,
  1112. );
  1113. try {
  1114. await _task.onStart(params);
  1115. } catch (e) {} finally {
  1116. // For now, we return successfully from AudioService.start regardless of
  1117. // whether an exception occurred in onStart.
  1118. await _backgroundChannel.invokeMethod('started');
  1119. }
  1120. }
  1121. /// Shuts down the background audio task within the background isolate.
  1122. static Future<void> _shutdown() async {
  1123. if (!_running) return;
  1124. // Set this to false immediately so that if duplicate shutdown requests come
  1125. // through, they are ignored.
  1126. _running = false;
  1127. final audioSession = await AudioSession.instance;
  1128. try {
  1129. await audioSession.setActive(false);
  1130. } catch (e) {
  1131. print("While deactivating audio session: $e");
  1132. }
  1133. await _backgroundChannel.invokeMethod('stopped');
  1134. if (kIsWeb) {
  1135. } else if (Platform.isIOS) {
  1136. FlutterIsolate.current?.kill();
  1137. }
  1138. _backgroundChannel.setMethodCallHandler(null);
  1139. _state = _noneState;
  1140. }
  1141. /// Broadcasts to all clients the current state, including:
  1142. ///
  1143. /// * Whether media is playing or paused
  1144. /// * Whether media is buffering or skipping
  1145. /// * The current position, buffered position and speed
  1146. /// * The current set of media actions that should be enabled
  1147. ///
  1148. /// Connected clients will use this information to update their UI.
  1149. ///
  1150. /// You should use [controls] to specify the set of clickable buttons that
  1151. /// should currently be visible in the notification in the current state,
  1152. /// where each button is a [MediaControl] that triggers a different
  1153. /// [MediaAction]. Only the following actions can be enabled as
  1154. /// [MediaControl]s:
  1155. ///
  1156. /// * [MediaAction.stop]
  1157. /// * [MediaAction.pause]
  1158. /// * [MediaAction.play]
  1159. /// * [MediaAction.rewind]
  1160. /// * [MediaAction.skipToPrevious]
  1161. /// * [MediaAction.skipToNext]
  1162. /// * [MediaAction.fastForward]
  1163. /// * [MediaAction.playPause]
  1164. ///
  1165. /// Any other action you would like to enable for clients that is not a clickable
  1166. /// notification button should be specified in the [systemActions] parameter. For
  1167. /// example:
  1168. ///
  1169. /// * [MediaAction.seekTo] (enable a seek bar)
  1170. /// * [MediaAction.seekForward] (enable press-and-hold fast-forward control)
  1171. /// * [MediaAction.seekBackward] (enable press-and-hold rewind control)
  1172. ///
  1173. /// In practice, iOS will treat all entries in [controls] and [systemActions]
  1174. /// in the same way since you cannot customise the icons of controls in the
  1175. /// Control Center. However, on Android, the distinction is important as clickable
  1176. /// buttons in the notification require you to specify your own icon.
  1177. ///
  1178. /// Note that specifying [MediaAction.seekTo] in [systemActions] will enable
  1179. /// a seek bar in both the Android notification and the iOS control center.
  1180. /// [MediaAction.seekForward] and [MediaAction.seekBackward] have a special
  1181. /// behaviour on iOS in which if you have already enabled the
  1182. /// [MediaAction.skipToNext] and [MediaAction.skipToPrevious] buttons, these
  1183. /// additional actions will allow the user to press and hold the buttons to
  1184. /// activate the continuous seeking behaviour.
  1185. ///
  1186. /// On Android, a media notification has a compact and expanded form. In the
  1187. /// compact view, you can optionally specify the indices of up to 3 of your
  1188. /// [controls] that you would like to be shown.
  1189. ///
  1190. /// The playback [position] should NOT be updated continuously in real time.
  1191. /// Instead, it should be updated only when the normal continuity of time is
  1192. /// disrupted, such as during a seek, buffering and seeking. When
  1193. /// broadcasting such a position change, the [updateTime] specifies the time
  1194. /// of that change, allowing clients to project the realtime value of the
  1195. /// position as `position + (DateTime.now() - updateTime)`. As a convenience,
  1196. /// this calculation is provided by [PlaybackState.currentPosition].
  1197. ///
  1198. /// The playback [speed] is given as a double where 1.0 means normal speed.
  1199. static Future<void> setState({
  1200. @required List<MediaControl> controls,
  1201. List<MediaAction> systemActions = const [],
  1202. @required AudioProcessingState processingState,
  1203. @required bool playing,
  1204. Duration position = Duration.zero,
  1205. Duration bufferedPosition = Duration.zero,
  1206. double speed = 1.0,
  1207. Duration updateTime,
  1208. List<int> androidCompactActions,
  1209. AudioServiceRepeatMode repeatMode = AudioServiceRepeatMode.none,
  1210. AudioServiceShuffleMode shuffleMode = AudioServiceShuffleMode.none,
  1211. }) async {
  1212. _state = PlaybackState(
  1213. processingState: processingState,
  1214. playing: playing,
  1215. actions: controls.map((control) => control.action).toSet(),
  1216. position: position,
  1217. bufferedPosition: bufferedPosition,
  1218. speed: speed,
  1219. updateTime: updateTime,
  1220. repeatMode: repeatMode,
  1221. shuffleMode: shuffleMode,
  1222. );
  1223. List<Map> rawControls = controls
  1224. .map((control) => {
  1225. 'androidIcon': control.androidIcon,
  1226. 'label': control.label,
  1227. 'action': control.action.index,
  1228. })
  1229. .toList();
  1230. final rawSystemActions =
  1231. systemActions.map((action) => action.index).toList();
  1232. await _backgroundChannel.invokeMethod('setState', [
  1233. rawControls,
  1234. rawSystemActions,
  1235. processingState?.index ?? AudioProcessingState.none.index,
  1236. playing ?? false,
  1237. position?.inMilliseconds ?? 0,
  1238. bufferedPosition?.inMilliseconds ?? 0,
  1239. speed ?? 1.0,
  1240. updateTime?.inMilliseconds,
  1241. androidCompactActions,
  1242. repeatMode?.index ?? AudioServiceRepeatMode.none.index,
  1243. shuffleMode?.index ?? AudioServiceShuffleMode.none.index,
  1244. ]);
  1245. }
  1246. /// Sets the current queue and notifies all clients.
  1247. static Future<void> setQueue(List<MediaItem> queue,
  1248. {bool preloadArtwork = false}) async {
  1249. _queue = queue;
  1250. if (preloadArtwork) {
  1251. _loadAllArtwork(queue);
  1252. }
  1253. await _backgroundChannel.invokeMethod(
  1254. 'setQueue', queue.map((item) => item.toJson()).toList());
  1255. }
  1256. /// Sets the currently playing media item and notifies all clients.
  1257. static Future<void> setMediaItem(MediaItem mediaItem) async {
  1258. _mediaItem = mediaItem;
  1259. if (mediaItem.artUri != null) {
  1260. // We potentially need to fetch the art.
  1261. String filePath = _getLocalPath(mediaItem.artUri);
  1262. if (filePath == null) {
  1263. final fileInfo = _cacheManager.getFileFromMemory(mediaItem.artUri);
  1264. filePath = fileInfo?.file?.path;
  1265. if (filePath == null) {
  1266. // We haven't fetched the art yet, so show the metadata now, and again
  1267. // after we load the art.
  1268. await _backgroundChannel.invokeMethod(
  1269. 'setMediaItem', mediaItem.toJson());
  1270. // Load the art
  1271. filePath = await _loadArtwork(mediaItem);
  1272. // If we failed to download the art, abort.
  1273. if (filePath == null) return;
  1274. // If we've already set a new media item, cancel this request.
  1275. if (mediaItem != _mediaItem) return;
  1276. }
  1277. }
  1278. final extras = Map.of(mediaItem.extras ?? <String, dynamic>{});
  1279. extras['artCacheFile'] = filePath;
  1280. final platformMediaItem = mediaItem.copyWith(extras: extras);
  1281. // Show the media item after the art is loaded.
  1282. await _backgroundChannel.invokeMethod(
  1283. 'setMediaItem', platformMediaItem.toJson());
  1284. } else {
  1285. await _backgroundChannel.invokeMethod('setMediaItem', mediaItem.toJson());
  1286. }
  1287. }
  1288. static Future<void> _loadAllArtwork(List<MediaItem> queue) async {
  1289. for (var mediaItem in queue) {
  1290. await _loadArtwork(mediaItem);
  1291. }
  1292. }
  1293. static Future<String> _loadArtwork(MediaItem mediaItem) async {
  1294. try {
  1295. final artUri = mediaItem.artUri;
  1296. if (artUri != null) {
  1297. String local = _getLocalPath(artUri);
  1298. if (local != null) {
  1299. return local;
  1300. } else {
  1301. final file = await _cacheManager.getSingleFile(mediaItem.artUri);
  1302. return file.path;
  1303. }
  1304. }
  1305. } catch (e) {}
  1306. return null;
  1307. }
  1308. static String _getLocalPath(String artUri) {
  1309. const prefix = "file://";
  1310. if (artUri.toLowerCase().startsWith(prefix)) {
  1311. return artUri.substring(prefix.length);
  1312. }
  1313. return null;
  1314. }
  1315. /// Notifies clients that the child media items of [parentMediaId] have
  1316. /// changed.
  1317. ///
  1318. /// If [parentMediaId] is unspecified, the root parent will be used.
  1319. static Future<void> notifyChildrenChanged(
  1320. [String parentMediaId = AudioService.MEDIA_ROOT_ID]) async {
  1321. await _backgroundChannel.invokeMethod(
  1322. 'notifyChildrenChanged', parentMediaId);
  1323. }
  1324. /// In Android, forces media button events to be routed to your active media
  1325. /// session.
  1326. ///
  1327. /// This is necessary if you want to play TextToSpeech in the background and
  1328. /// still respond to media button events. You should call it just before
  1329. /// playing TextToSpeech.
  1330. ///
  1331. /// This is not necessary if you are playing normal audio in the background
  1332. /// such as music because this kind of "normal" audio playback will
  1333. /// automatically qualify your app to receive media button events.
  1334. static Future<void> androidForceEnableMediaButtons() async {
  1335. await _backgroundChannel.invokeMethod('androidForceEnableMediaButtons');
  1336. }
  1337. /// Sends a custom event to the Flutter UI.
  1338. ///
  1339. /// The event parameter can contain any data permitted by Dart's
  1340. /// SendPort/ReceivePort API. Please consult the relevant documentation for
  1341. /// further information.
  1342. static void sendCustomEvent(dynamic event) {
  1343. if (!AudioService.usesIsolate) {
  1344. AudioService._customEventSubject.add(event);
  1345. } else {
  1346. SendPort sendPort =
  1347. IsolateNameServer.lookupPortByName(_CUSTOM_EVENT_PORT_NAME);
  1348. sendPort?.send(event);
  1349. }
  1350. }
  1351. }
  1352. /// An audio task that can run in the background and react to audio events.
  1353. ///
  1354. /// You should subclass [BackgroundAudioTask] and override the callbacks for
  1355. /// each type of event that your background task wishes to react to. At a
  1356. /// minimum, you must override [onStart] and [onStop] to handle initialising
  1357. /// and shutting down the audio task.
  1358. abstract class BackgroundAudioTask {
  1359. final BaseCacheManager cacheManager;
  1360. Duration _fastForwardInterval;
  1361. Duration _rewindInterval;
  1362. /// Subclasses may supply a [cacheManager] to manage the loading of artwork,
  1363. /// or an instance of [DefaultCacheManager] will be used by default.
  1364. BackgroundAudioTask({BaseCacheManager cacheManager})
  1365. : this.cacheManager = cacheManager ?? DefaultCacheManager();
  1366. /// The fast forward interval passed into [AudioService.start].
  1367. Duration get fastForwardInterval => _fastForwardInterval;
  1368. /// The rewind interval passed into [AudioService.start].
  1369. Duration get rewindInterval => _rewindInterval;
  1370. /// Called once when this audio task is first started and ready to play
  1371. /// audio, in response to [AudioService.start]. [params] will contain any
  1372. /// params passed into [AudioService.start] when starting this background
  1373. /// audio task.
  1374. Future<void> onStart(Map<String, dynamic> params) async {}
  1375. /// Called when a client has requested to terminate this background audio
  1376. /// task, in response to [AudioService.stop]. You should implement this
  1377. /// method to stop playing audio and dispose of any resources used.
  1378. ///
  1379. /// If you override this, make sure your method ends with a call to `await
  1380. /// super.onStop()`. The isolate containing this task will shut down as soon
  1381. /// as this method completes.
  1382. @mustCallSuper
  1383. Future<void> onStop() async {
  1384. await AudioServiceBackground._shutdown();
  1385. }
  1386. /// Called when a media browser client, such as Android Auto, wants to query
  1387. /// the available media items to display to the user.
  1388. Future<List<MediaItem>> onLoadChildren(String parentMediaId) async => [];
  1389. /// Called when the media button on the headset is pressed, or in response to
  1390. /// a call from [AudioService.click]. The default behaviour is:
  1391. ///
  1392. /// * On [MediaButton.media], toggle [onPlay] and [onPause].
  1393. /// * On [MediaButton.next], call [onSkipToNext].
  1394. /// * On [MediaButton.previous], call [onSkipToPrevious].
  1395. Future<void> onClick(MediaButton button) async {
  1396. switch (button) {
  1397. case MediaButton.media:
  1398. if (AudioServiceBackground.state?.playing == true) {
  1399. await onPause();
  1400. } else {
  1401. await onPlay();
  1402. }
  1403. break;
  1404. case MediaButton.next:
  1405. await onSkipToNext();
  1406. break;
  1407. case MediaButton.previous:
  1408. await onSkipToPrevious();
  1409. break;
  1410. }
  1411. }
  1412. /// Called when a client has requested to pause audio playback, such as via a
  1413. /// call to [AudioService.pause]. You should implement this method to pause
  1414. /// audio playback and also broadcast the appropriate state change via
  1415. /// [AudioServiceBackground.setState].
  1416. Future<void> onPause() async {}
  1417. /// Called when a client has requested to prepare audio for playback, such as
  1418. /// via a call to [AudioService.prepare].
  1419. Future<void> onPrepare() async {}
  1420. /// Called when a client has requested to prepare a specific media item for
  1421. /// audio playback, such as via a call to [AudioService.prepareFromMediaId].
  1422. Future<void> onPrepareFromMediaId(String mediaId) async {}
  1423. /// Called when a client has requested to resume audio playback, such as via
  1424. /// a call to [AudioService.play]. You should implement this method to play
  1425. /// audio and also broadcast the appropriate state change via
  1426. /// [AudioServiceBackground.setState].
  1427. Future<void> onPlay() async {}
  1428. /// Called when a client has requested to play a media item by its ID, such
  1429. /// as via a call to [AudioService.playFromMediaId]. You should implement
  1430. /// this method to play audio and also broadcast the appropriate state change
  1431. /// via [AudioServiceBackground.setState].
  1432. Future<void> onPlayFromMediaId(String mediaId) async {}
  1433. /// Called when the Flutter UI has requested to play a given media item via a
  1434. /// call to [AudioService.playMediaItem]. You should implement this method to
  1435. /// play audio and also broadcast the appropriate state change via
  1436. /// [AudioServiceBackground.setState].
  1437. ///
  1438. /// Note: This method can only be triggered by your Flutter UI. Peripheral
  1439. /// devices such as Android Auto will instead trigger
  1440. /// [AudioService.onPlayFromMediaId].
  1441. Future<void> onPlayMediaItem(MediaItem mediaItem) async {}
  1442. /// Called when a client has requested to add a media item to the queue, such
  1443. /// as via a call to [AudioService.addQueueItem].
  1444. Future<void> onAddQueueItem(MediaItem mediaItem) async {}
  1445. /// Called when the Flutter UI has requested to set a new queue.
  1446. ///
  1447. /// If you use a queue, your implementation of this method should call
  1448. /// [AudioServiceBackground.setQueue] to notify all clients that the queue
  1449. /// has changed.
  1450. Future<void> onUpdateQueue(List<MediaItem> queue) async {}
  1451. /// Called when the Flutter UI has requested to update the details of
  1452. /// a media item.
  1453. Future<void> onUpdateMediaItem(MediaItem mediaItem) async {}
  1454. /// Called when a client has requested to add a media item to the queue at a
  1455. /// specified position, such as via a request to
  1456. /// [AudioService.addQueueItemAt].
  1457. Future<void> onAddQueueItemAt(MediaItem mediaItem, int index) async {}
  1458. /// Called when a client has requested to remove a media item from the queue,
  1459. /// such as via a request to [AudioService.removeQueueItem].
  1460. Future<void> onRemoveQueueItem(MediaItem mediaItem) async {}
  1461. /// Called when a client has requested to skip to the next item in the queue,
  1462. /// such as via a request to [AudioService.skipToNext].
  1463. ///
  1464. /// By default, calls [onSkipToQueueItem] with the queue item after
  1465. /// [AudioServiceBackground.mediaItem] if it exists.
  1466. Future<void> onSkipToNext() => _skip(1);
  1467. /// Called when a client has requested to skip to the previous item in the
  1468. /// queue, such as via a request to [AudioService.skipToPrevious].
  1469. ///
  1470. /// By default, calls [onSkipToQueueItem] with the queue item before
  1471. /// [AudioServiceBackground.mediaItem] if it exists.
  1472. Future<void> onSkipToPrevious() => _skip(-1);
  1473. /// Called when a client has requested to fast forward, such as via a
  1474. /// request to [AudioService.fastForward]. An implementation of this callback
  1475. /// can use the [fastForwardInterval] property to determine how much audio
  1476. /// to skip.
  1477. Future<void> onFastForward() async {}
  1478. /// Called when a client has requested to rewind, such as via a request to
  1479. /// [AudioService.rewind]. An implementation of this callback can use the
  1480. /// [rewindInterval] property to determine how much audio to skip.
  1481. Future<void> onRewind() async {}
  1482. /// Called when a client has requested to skip to a specific item in the
  1483. /// queue, such as via a call to [AudioService.skipToQueueItem].
  1484. Future<void> onSkipToQueueItem(String mediaId) async {}
  1485. /// Called when a client has requested to seek to a position, such as via a
  1486. /// call to [AudioService.seekTo]. If your implementation of seeking causes
  1487. /// buffering to occur, consider broadcasting a buffering state via
  1488. /// [AudioServiceBackground.setState] while the seek is in progress.
  1489. Future<void> onSeekTo(Duration position) async {}
  1490. /// Called when a client has requested to rate the current media item, such as
  1491. /// via a call to [AudioService.setRating].
  1492. Future<void> onSetRating(Rating rating, Map<dynamic, dynamic> extras) async {}
  1493. /// Called when a client has requested to change the current repeat mode.
  1494. Future<void> onSetRepeatMode(AudioServiceRepeatMode repeatMode) async {}
  1495. /// Called when a client has requested to change the current shuffle mode.
  1496. Future<void> onSetShuffleMode(AudioServiceShuffleMode shuffleMode) async {}
  1497. /// Called when a client has requested to either begin or end seeking
  1498. /// backward.
  1499. Future<void> onSeekBackward(bool begin) async {}
  1500. /// Called when a client has requested to either begin or end seeking
  1501. /// forward.
  1502. Future<void> onSeekForward(bool begin) async {}
  1503. /// Called when the Flutter UI has requested to set the speed of audio
  1504. /// playback. An implementation of this callback should change the audio
  1505. /// speed and broadcast the speed change to all clients via
  1506. /// [AudioServiceBackground.setState].
  1507. Future<void> onSetSpeed(double speed) async {}
  1508. /// Called when a custom action has been sent by the client via
  1509. /// [AudioService.customAction]. The result of this method will be returned
  1510. /// to the client.
  1511. Future<dynamic> onCustomAction(String name, dynamic arguments) async {}
  1512. /// Called on Android when the user swipes away your app's task in the task
  1513. /// manager. Note that if you use the `androidStopForegroundOnPause` option to
  1514. /// [AudioService.start], then when your audio is paused, the operating
  1515. /// system moves your service to a lower priority level where it can be
  1516. /// destroyed at any time to reclaim memory. If the user swipes away your
  1517. /// task under these conditions, the operating system will destroy your
  1518. /// service, and you may override this method to do any cleanup. For example:
  1519. ///
  1520. /// ```dart
  1521. /// void onTaskRemoved() {
  1522. /// if (!AudioServiceBackground.state.playing) {
  1523. /// onStop();
  1524. /// }
  1525. /// }
  1526. /// ```
  1527. Future<void> onTaskRemoved() async {}
  1528. /// Called on Android when the user swipes away the notification. The default
  1529. /// implementation (which you may override) calls [onStop]. Note that by
  1530. /// default, the service runs in the foreground state which (despite the name)
  1531. /// allows the service to run at a high priority in the background without the
  1532. /// operating system killing it. While in the foreground state, the
  1533. /// notification cannot be swiped away. You can pass a parameter value of
  1534. /// `true` for `androidStopForegroundOnPause` in the [AudioService.start]
  1535. /// method if you would like the service to exit the foreground state when
  1536. /// playback is paused. This will allow the user to swipe the notification
  1537. /// away while playback is paused (but it will also allow the operating system
  1538. /// to kill your service at any time to free up resources).
  1539. Future<void> onClose() => onStop();
  1540. void _setParams({
  1541. Duration fastForwardInterval,
  1542. Duration rewindInterval,
  1543. }) {
  1544. _fastForwardInterval = fastForwardInterval;
  1545. _rewindInterval = rewindInterval;
  1546. }
  1547. Future<void> _skip(int offset) async {
  1548. final mediaItem = AudioServiceBackground.mediaItem;
  1549. if (mediaItem == null) return;
  1550. final queue = AudioServiceBackground.queue ?? [];
  1551. int i = queue.indexOf(mediaItem);
  1552. if (i == -1) return;
  1553. int newIndex = i + offset;
  1554. if (newIndex >= 0 && newIndex < queue.length)
  1555. await onSkipToQueueItem(queue[newIndex]?.id);
  1556. }
  1557. }
  1558. _iosIsolateEntrypoint(int rawHandle) async {
  1559. ui.CallbackHandle handle = ui.CallbackHandle.fromRawHandle(rawHandle);
  1560. Function backgroundTask = ui.PluginUtilities.getCallbackFromHandle(handle);
  1561. backgroundTask();
  1562. }
  1563. /// A widget that maintains a connection to [AudioService].
  1564. ///
  1565. /// Insert this widget at the top of your `/` route's widget tree to maintain
  1566. /// the connection across all routes. e.g.
  1567. ///
  1568. /// ```
  1569. /// return MaterialApp(
  1570. /// home: AudioServiceWidget(MainScreen()),
  1571. /// );
  1572. /// ```
  1573. ///
  1574. /// Note that this widget will not work if it wraps around [MateriaApp] itself,
  1575. /// you must place it in the widget tree within your route.
  1576. class AudioServiceWidget extends StatefulWidget {
  1577. final Widget child;
  1578. AudioServiceWidget({@required this.child});
  1579. @override
  1580. _AudioServiceWidgetState createState() => _AudioServiceWidgetState();
  1581. }
  1582. class _AudioServiceWidgetState extends State<AudioServiceWidget>
  1583. with WidgetsBindingObserver {
  1584. @override
  1585. void initState() {
  1586. super.initState();
  1587. WidgetsBinding.instance.addObserver(this);
  1588. AudioService.connect();
  1589. }
  1590. @override
  1591. void dispose() {
  1592. AudioService.disconnect();
  1593. WidgetsBinding.instance.removeObserver(this);
  1594. super.dispose();
  1595. }
  1596. @override
  1597. void didChangeAppLifecycleState(AppLifecycleState state) {
  1598. switch (state) {
  1599. case AppLifecycleState.resumed:
  1600. AudioService.connect();
  1601. break;
  1602. case AppLifecycleState.paused:
  1603. AudioService.disconnect();
  1604. break;
  1605. default:
  1606. break;
  1607. }
  1608. }
  1609. @override
  1610. Future<bool> didPopRoute() async {
  1611. AudioService.disconnect();
  1612. return false;
  1613. }
  1614. @override
  1615. Widget build(BuildContext context) {
  1616. return widget.child;
  1617. }
  1618. }
  1619. enum AudioServiceShuffleMode { none, all, group }
  1620. enum AudioServiceRepeatMode { none, one, all, group }
  1621. class _AsyncTaskQueue {
  1622. final _queuedAsyncTaskController = StreamController<_AsyncTaskQueueEntry>();
  1623. _AsyncTaskQueue() {
  1624. _process();
  1625. }
  1626. Future<void> _process() async {
  1627. await for (var entry in _queuedAsyncTaskController.stream) {
  1628. try {
  1629. final result = await entry.asyncTask();
  1630. entry.completer.complete(result);
  1631. } catch (e, stacktrace) {
  1632. entry.completer.completeError(e, stacktrace);
  1633. }
  1634. }
  1635. }
  1636. Future<dynamic> schedule(_AsyncTask asyncTask) async {
  1637. final completer = Completer<dynamic>();
  1638. _queuedAsyncTaskController.add(_AsyncTaskQueueEntry(asyncTask, completer));
  1639. return completer.future;
  1640. }
  1641. }
  1642. class _AsyncTaskQueueEntry {
  1643. final _AsyncTask asyncTask;
  1644. final Completer completer;
  1645. _AsyncTaskQueueEntry(this.asyncTask, this.completer);
  1646. }
  1647. typedef _AsyncTask = Future<dynamic> Function();