just_audio_web.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. import 'dart:async';
  2. import 'dart:html';
  3. import 'dart:math';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter/widgets.dart';
  6. import 'package:flutter_web_plugins/flutter_web_plugins.dart';
  7. import 'package:just_audio/just_audio.dart';
  8. final Random _random = Random();
  9. class JustAudioPlugin {
  10. static void registerWith(Registrar registrar) {
  11. final MethodChannel channel = MethodChannel(
  12. 'com.ryanheise.just_audio.methods',
  13. const StandardMethodCodec(),
  14. registrar.messenger);
  15. final JustAudioPlugin instance = JustAudioPlugin(registrar);
  16. channel.setMethodCallHandler(instance.handleMethodCall);
  17. }
  18. final Registrar registrar;
  19. JustAudioPlugin(this.registrar);
  20. Future<dynamic> handleMethodCall(MethodCall call) async {
  21. switch (call.method) {
  22. case 'init':
  23. final String id = call.arguments[0];
  24. new Html5AudioPlayer(id: id, registrar: registrar);
  25. return null;
  26. case 'setIosCategory':
  27. return null;
  28. default:
  29. throw PlatformException(code: 'Unimplemented');
  30. }
  31. }
  32. }
  33. abstract class JustAudioPlayer {
  34. final String id;
  35. final Registrar registrar;
  36. final MethodChannel methodChannel;
  37. final PluginEventChannel eventChannel;
  38. final StreamController eventController = StreamController();
  39. ProcessingState _processingState = ProcessingState.none;
  40. bool _playing = false;
  41. int _index;
  42. JustAudioPlayer({@required this.id, @required this.registrar})
  43. : methodChannel = MethodChannel('com.ryanheise.just_audio.methods.$id',
  44. const StandardMethodCodec(), registrar.messenger),
  45. eventChannel = PluginEventChannel('com.ryanheise.just_audio.events.$id',
  46. const StandardMethodCodec(), registrar.messenger) {
  47. methodChannel.setMethodCallHandler(_methodHandler);
  48. eventChannel.controller = eventController;
  49. }
  50. Future<dynamic> _methodHandler(MethodCall call) async {
  51. try {
  52. final args = call.arguments;
  53. switch (call.method) {
  54. case 'load':
  55. return await load(args[0]);
  56. case 'play':
  57. return await play();
  58. case 'pause':
  59. return await pause();
  60. case 'setVolume':
  61. return await setVolume(args[0]);
  62. case 'setSpeed':
  63. return await setSpeed(args[0]);
  64. case 'setLoopMode':
  65. return await setLoopMode(args[0]);
  66. case 'setShuffleModeEnabled':
  67. return await setShuffleModeEnabled(args[0]);
  68. case 'setAutomaticallyWaitsToMinimizeStalling':
  69. return null;
  70. case 'seek':
  71. return await seek(args[0], args[1]);
  72. case 'dispose':
  73. return dispose();
  74. case 'concatenating.add':
  75. return await concatenatingAdd(args[0], args[1]);
  76. case "concatenating.insert":
  77. return await concatenatingInsert(args[0], args[1], args[2]);
  78. case "concatenating.addAll":
  79. return await concatenatingAddAll(args[0], args[1]);
  80. case "concatenating.insertAll":
  81. return await concatenatingInsertAll(args[0], args[1], args[2]);
  82. case "concatenating.removeAt":
  83. return await concatenatingRemoveAt(args[0], args[1]);
  84. case "concatenating.removeRange":
  85. return await concatenatingRemoveRange(args[0], args[1], args[2]);
  86. case "concatenating.move":
  87. return await concatenatingMove(args[0], args[1], args[2]);
  88. case "concatenating.clear":
  89. return await concatenatingClear(args[0]);
  90. case "setAndroidAudioAttributes":
  91. return null;
  92. default:
  93. throw PlatformException(code: 'Unimplemented');
  94. }
  95. } catch (e, stacktrace) {
  96. print("$stacktrace");
  97. rethrow;
  98. }
  99. }
  100. Future<int> load(Map source);
  101. Future<void> play();
  102. Future<void> pause();
  103. Future<void> setVolume(double volume);
  104. Future<void> setSpeed(double speed);
  105. Future<void> setLoopMode(int mode);
  106. Future<void> setShuffleModeEnabled(bool enabled);
  107. Future<void> seek(int position, int index);
  108. @mustCallSuper
  109. void dispose() {
  110. eventController.close();
  111. }
  112. Duration getCurrentPosition();
  113. Duration getBufferedPosition();
  114. Duration getDuration();
  115. concatenatingAdd(String playerId, Map source);
  116. concatenatingInsert(String playerId, int index, Map source);
  117. concatenatingAddAll(String playerId, List sources);
  118. concatenatingInsertAll(String playerId, int index, List sources);
  119. concatenatingRemoveAt(String playerId, int index);
  120. concatenatingRemoveRange(String playerId, int start, int end);
  121. concatenatingMove(String playerId, int currentIndex, int newIndex);
  122. concatenatingClear(String playerId);
  123. broadcastPlaybackEvent() {
  124. var updateTime = DateTime.now().millisecondsSinceEpoch;
  125. eventController.add({
  126. 'processingState': _processingState.index,
  127. 'updatePosition': getCurrentPosition()?.inMilliseconds,
  128. 'updateTime': updateTime,
  129. 'bufferedPosition': getBufferedPosition()?.inMilliseconds,
  130. // TODO: Icy Metadata
  131. 'icyMetadata': null,
  132. 'duration': getDuration()?.inMilliseconds,
  133. 'currentIndex': _index,
  134. });
  135. }
  136. transition(ProcessingState processingState) {
  137. _processingState = processingState;
  138. broadcastPlaybackEvent();
  139. }
  140. }
  141. class Html5AudioPlayer extends JustAudioPlayer {
  142. AudioElement _audioElement = AudioElement();
  143. Completer _durationCompleter;
  144. AudioSourcePlayer _audioSourcePlayer;
  145. LoopMode _loopMode = LoopMode.off;
  146. bool _shuffleModeEnabled = false;
  147. final Map<String, AudioSourcePlayer> _audioSourcePlayers = {};
  148. Html5AudioPlayer({@required String id, @required Registrar registrar})
  149. : super(id: id, registrar: registrar) {
  150. _audioElement.addEventListener('durationchange', (event) {
  151. _durationCompleter?.complete();
  152. broadcastPlaybackEvent();
  153. });
  154. _audioElement.addEventListener('error', (event) {
  155. _durationCompleter?.completeError(_audioElement.error);
  156. });
  157. _audioElement.addEventListener('ended', (event) async {
  158. _currentAudioSourcePlayer.complete();
  159. });
  160. _audioElement.addEventListener('timeupdate', (event) {
  161. _currentAudioSourcePlayer.timeUpdated(_audioElement.currentTime);
  162. });
  163. _audioElement.addEventListener('loadstart', (event) {
  164. transition(ProcessingState.buffering);
  165. });
  166. _audioElement.addEventListener('waiting', (event) {
  167. transition(ProcessingState.buffering);
  168. });
  169. _audioElement.addEventListener('stalled', (event) {
  170. transition(ProcessingState.buffering);
  171. });
  172. _audioElement.addEventListener('canplaythrough', (event) {
  173. transition(ProcessingState.ready);
  174. });
  175. _audioElement.addEventListener('progress', (event) {
  176. broadcastPlaybackEvent();
  177. });
  178. }
  179. List<int> get order {
  180. final sequence = _audioSourcePlayer.sequence;
  181. List<int> order = List<int>(sequence.length);
  182. if (_shuffleModeEnabled) {
  183. order = _audioSourcePlayer.shuffleOrder;
  184. } else {
  185. for (var i = 0; i < order.length; i++) {
  186. order[i] = i;
  187. }
  188. }
  189. return order;
  190. }
  191. List<int> getInv(List<int> order) {
  192. List<int> orderInv = List<int>(order.length);
  193. for (var i = 0; i < order.length; i++) {
  194. orderInv[order[i]] = i;
  195. }
  196. return orderInv;
  197. }
  198. onEnded() async {
  199. if (_loopMode == LoopMode.one) {
  200. await seek(0, null);
  201. play();
  202. } else {
  203. final order = this.order;
  204. final orderInv = getInv(order);
  205. if (orderInv[_index] + 1 < order.length) {
  206. // move to next item
  207. _index = order[orderInv[_index] + 1];
  208. await _currentAudioSourcePlayer.load();
  209. // Should always be true...
  210. if (_playing) {
  211. play();
  212. }
  213. } else {
  214. // reached end of playlist
  215. if (_loopMode == LoopMode.all) {
  216. // Loop back to the beginning
  217. if (order.length == 1) {
  218. await seek(0, null);
  219. play();
  220. } else {
  221. _index = order[0];
  222. await _currentAudioSourcePlayer.load();
  223. // Should always be true...
  224. if (_playing) {
  225. play();
  226. }
  227. }
  228. } else {
  229. transition(ProcessingState.completed);
  230. }
  231. }
  232. }
  233. }
  234. // TODO: Improve efficiency.
  235. IndexedAudioSourcePlayer get _currentAudioSourcePlayer =>
  236. _audioSourcePlayer != null && _index < _audioSourcePlayer.sequence.length
  237. ? _audioSourcePlayer.sequence[_index]
  238. : null;
  239. @override
  240. Future<int> load(Map source) async {
  241. _currentAudioSourcePlayer?.pause();
  242. _audioSourcePlayer = getAudioSource(source);
  243. _index = 0;
  244. if (_shuffleModeEnabled) {
  245. _audioSourcePlayer?.shuffle(0, _index);
  246. }
  247. return (await _currentAudioSourcePlayer.load())?.inMilliseconds;
  248. }
  249. Future<Duration> loadUri(final Uri uri) async {
  250. transition(ProcessingState.loading);
  251. final src = uri.toString();
  252. if (src != _audioElement.src) {
  253. _durationCompleter = Completer<num>();
  254. _audioElement.src = src;
  255. _audioElement.preload = 'auto';
  256. _audioElement.load();
  257. try {
  258. await _durationCompleter.future;
  259. } on MediaError catch (e) {
  260. throw PlatformException(
  261. code: "${e.code}", message: "Failed to load URL");
  262. } finally {
  263. _durationCompleter = null;
  264. }
  265. }
  266. transition(ProcessingState.ready);
  267. final seconds = _audioElement.duration;
  268. return seconds.isFinite
  269. ? Duration(milliseconds: (seconds * 1000).toInt())
  270. : null;
  271. }
  272. @override
  273. Future<void> play() async {
  274. _playing = true;
  275. await _currentAudioSourcePlayer.play();
  276. }
  277. @override
  278. Future<void> pause() async {
  279. _playing = false;
  280. _currentAudioSourcePlayer.pause();
  281. }
  282. @override
  283. Future<void> setVolume(double volume) async {
  284. _audioElement.volume = volume;
  285. }
  286. @override
  287. Future<void> setSpeed(double speed) async {
  288. _audioElement.playbackRate = speed;
  289. }
  290. @override
  291. Future<void> setLoopMode(int mode) async {
  292. _loopMode = LoopMode.values[mode];
  293. }
  294. @override
  295. Future<void> setShuffleModeEnabled(bool enabled) async {
  296. _shuffleModeEnabled = enabled;
  297. if (enabled) {
  298. _audioSourcePlayer?.shuffle(0, _index);
  299. }
  300. }
  301. @override
  302. Future<void> seek(int position, int newIndex) async {
  303. int index = newIndex ?? _index;
  304. if (index != _index) {
  305. _currentAudioSourcePlayer.pause();
  306. _index = index;
  307. await _currentAudioSourcePlayer.load();
  308. await _currentAudioSourcePlayer.seek(position);
  309. if (_playing) {
  310. _currentAudioSourcePlayer.play();
  311. }
  312. } else {
  313. await _currentAudioSourcePlayer.seek(position);
  314. }
  315. }
  316. ConcatenatingAudioSourcePlayer _concatenating(String playerId) =>
  317. _audioSourcePlayers[playerId] as ConcatenatingAudioSourcePlayer;
  318. concatenatingAdd(String playerId, Map source) {
  319. final playlist = _concatenating(playerId);
  320. playlist.add(getAudioSource(source));
  321. }
  322. concatenatingInsert(String playerId, int index, Map source) {
  323. _concatenating(playerId).insert(index, getAudioSource(source));
  324. if (index <= _index) {
  325. _index++;
  326. }
  327. }
  328. concatenatingAddAll(String playerId, List sources) {
  329. _concatenating(playerId).addAll(getAudioSources(sources));
  330. }
  331. concatenatingInsertAll(String playerId, int index, List sources) {
  332. _concatenating(playerId).insertAll(index, getAudioSources(sources));
  333. if (index <= _index) {
  334. _index += sources.length;
  335. }
  336. }
  337. concatenatingRemoveAt(String playerId, int index) async {
  338. // Pause if removing current item
  339. if (_index == index && _playing) {
  340. _currentAudioSourcePlayer.pause();
  341. }
  342. _concatenating(playerId).removeAt(index);
  343. if (_index == index) {
  344. // Skip backward if there's nothing after this
  345. if (index == _audioSourcePlayer.sequence.length) {
  346. _index--;
  347. }
  348. // Resume playback at the new item (if it exists)
  349. if (_playing && _currentAudioSourcePlayer != null) {
  350. await _currentAudioSourcePlayer.load();
  351. _currentAudioSourcePlayer.play();
  352. }
  353. } else if (index < _index) {
  354. // Reflect that the current item has shifted its position
  355. _index--;
  356. }
  357. }
  358. concatenatingRemoveRange(String playerId, int start, int end) async {
  359. if (_index >= start && _index < end && _playing) {
  360. // Pause if removing current item
  361. _currentAudioSourcePlayer.pause();
  362. }
  363. _concatenating(playerId).removeRange(start, end);
  364. if (_index >= start && _index < end) {
  365. // Skip backward if there's nothing after this
  366. if (start >= _audioSourcePlayer.sequence.length) {
  367. _index = start - 1;
  368. } else {
  369. _index = start;
  370. }
  371. // Resume playback at the new item (if it exists)
  372. if (_playing && _currentAudioSourcePlayer != null) {
  373. await _currentAudioSourcePlayer.load();
  374. _currentAudioSourcePlayer.play();
  375. }
  376. } else if (end <= _index) {
  377. // Reflect that the current item has shifted its position
  378. _index -= (end - start);
  379. }
  380. }
  381. concatenatingMove(String playerId, int currentIndex, int newIndex) {
  382. _concatenating(playerId).move(currentIndex, newIndex);
  383. if (currentIndex == _index) {
  384. _index = newIndex;
  385. } else if (currentIndex < _index && newIndex >= _index) {
  386. _index--;
  387. } else if (currentIndex > _index && newIndex <= _index) {
  388. _index++;
  389. }
  390. }
  391. concatenatingClear(String playerId) {
  392. _currentAudioSourcePlayer.pause();
  393. _concatenating(playerId).clear();
  394. }
  395. @override
  396. Duration getCurrentPosition() => _currentAudioSourcePlayer?.position;
  397. @override
  398. Duration getBufferedPosition() => _currentAudioSourcePlayer?.bufferedPosition;
  399. @override
  400. Duration getDuration() => _currentAudioSourcePlayer?.duration;
  401. @override
  402. void dispose() {
  403. _currentAudioSourcePlayer?.pause();
  404. _audioElement.removeAttribute('src');
  405. _audioElement.load();
  406. transition(ProcessingState.none);
  407. super.dispose();
  408. }
  409. List<AudioSourcePlayer> getAudioSources(List json) =>
  410. json.map((s) => getAudioSource(s)).toList();
  411. AudioSourcePlayer getAudioSource(Map json) {
  412. final String id = json['id'];
  413. var audioSourcePlayer = _audioSourcePlayers[id];
  414. if (audioSourcePlayer == null) {
  415. audioSourcePlayer = decodeAudioSource(json);
  416. _audioSourcePlayers[id] = audioSourcePlayer;
  417. }
  418. return audioSourcePlayer;
  419. }
  420. AudioSourcePlayer decodeAudioSource(Map json) {
  421. try {
  422. switch (json['type']) {
  423. case 'progressive':
  424. return ProgressiveAudioSourcePlayer(
  425. this, json['id'], Uri.parse(json['uri']), json['headers']);
  426. case "dash":
  427. return DashAudioSourcePlayer(
  428. this, json['id'], Uri.parse(json['uri']), json['headers']);
  429. case "hls":
  430. return HlsAudioSourcePlayer(
  431. this, json['id'], Uri.parse(json['uri']), json['headers']);
  432. case "concatenating":
  433. return ConcatenatingAudioSourcePlayer(
  434. this,
  435. json['id'],
  436. getAudioSources(json['audioSources']),
  437. json['useLazyPreparation']);
  438. case "clipping":
  439. return ClippingAudioSourcePlayer(
  440. this,
  441. json['id'],
  442. getAudioSource(json['audioSource']),
  443. Duration(milliseconds: json['start']),
  444. Duration(milliseconds: json['end']));
  445. case "looping":
  446. return LoopingAudioSourcePlayer(this, json['id'],
  447. getAudioSource(json['audioSource']), json['count']);
  448. default:
  449. throw Exception("Unknown AudioSource type: " + json['type']);
  450. }
  451. } catch (e, stacktrace) {
  452. print("$stacktrace");
  453. rethrow;
  454. }
  455. }
  456. }
  457. abstract class AudioSourcePlayer {
  458. Html5AudioPlayer html5AudioPlayer;
  459. final String id;
  460. AudioSourcePlayer(this.html5AudioPlayer, this.id);
  461. List<IndexedAudioSourcePlayer> get sequence;
  462. List<int> get shuffleOrder;
  463. int shuffle(int treeIndex, int currentIndex);
  464. }
  465. abstract class IndexedAudioSourcePlayer extends AudioSourcePlayer {
  466. IndexedAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id)
  467. : super(html5AudioPlayer, id);
  468. Future<Duration> load();
  469. Future<void> play();
  470. Future<void> pause();
  471. Future<void> seek(int position);
  472. Future<void> complete();
  473. Future<void> timeUpdated(double seconds) async {}
  474. Duration get duration;
  475. Duration get position;
  476. Duration get bufferedPosition;
  477. AudioElement get _audioElement => html5AudioPlayer._audioElement;
  478. @override
  479. int shuffle(int treeIndex, int currentIndex) => treeIndex + 1;
  480. @override
  481. String toString() => "${this.runtimeType}";
  482. }
  483. abstract class UriAudioSourcePlayer extends IndexedAudioSourcePlayer {
  484. final Uri uri;
  485. final Map headers;
  486. double _resumePos;
  487. Duration _duration;
  488. Completer _completer;
  489. UriAudioSourcePlayer(
  490. Html5AudioPlayer html5AudioPlayer, String id, this.uri, this.headers)
  491. : super(html5AudioPlayer, id);
  492. @override
  493. List<IndexedAudioSourcePlayer> get sequence => [this];
  494. @override
  495. List<int> get shuffleOrder => [0];
  496. @override
  497. Future<Duration> load() async {
  498. _resumePos = 0.0;
  499. return _duration = await html5AudioPlayer.loadUri(uri);
  500. }
  501. @override
  502. Future<void> play() async {
  503. _audioElement.currentTime = _resumePos;
  504. _audioElement.play();
  505. _completer = Completer();
  506. await _completer.future;
  507. _completer = null;
  508. }
  509. @override
  510. Future<void> pause() async {
  511. _resumePos = _audioElement.currentTime;
  512. _audioElement.pause();
  513. _interruptPlay();
  514. }
  515. @override
  516. Future<void> seek(int position) async {
  517. _audioElement.currentTime = _resumePos = position / 1000.0;
  518. }
  519. @override
  520. Future<void> complete() async {
  521. _interruptPlay();
  522. html5AudioPlayer.onEnded();
  523. }
  524. _interruptPlay() {
  525. if (_completer?.isCompleted == false) {
  526. _completer.complete();
  527. }
  528. }
  529. @override
  530. Duration get duration {
  531. return _duration;
  532. //final seconds = _audioElement.duration;
  533. //return seconds.isFinite
  534. // ? Duration(milliseconds: (seconds * 1000).toInt())
  535. // : null;
  536. }
  537. @override
  538. Duration get position {
  539. double seconds = _audioElement.currentTime;
  540. return Duration(milliseconds: (seconds * 1000).toInt());
  541. }
  542. @override
  543. Duration get bufferedPosition {
  544. if (_audioElement.buffered.length > 0) {
  545. return Duration(
  546. milliseconds:
  547. (_audioElement.buffered.end(_audioElement.buffered.length - 1) *
  548. 1000)
  549. .toInt());
  550. } else {
  551. return Duration.zero;
  552. }
  553. }
  554. }
  555. class ProgressiveAudioSourcePlayer extends UriAudioSourcePlayer {
  556. ProgressiveAudioSourcePlayer(
  557. Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers)
  558. : super(html5AudioPlayer, id, uri, headers);
  559. }
  560. class DashAudioSourcePlayer extends UriAudioSourcePlayer {
  561. DashAudioSourcePlayer(
  562. Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers)
  563. : super(html5AudioPlayer, id, uri, headers);
  564. }
  565. class HlsAudioSourcePlayer extends UriAudioSourcePlayer {
  566. HlsAudioSourcePlayer(
  567. Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers)
  568. : super(html5AudioPlayer, id, uri, headers);
  569. }
  570. class ConcatenatingAudioSourcePlayer extends AudioSourcePlayer {
  571. static List<int> generateShuffleOrder(int length, [int firstIndex]) {
  572. final shuffleOrder = List<int>(length);
  573. for (var i = 0; i < length; i++) {
  574. final j = _random.nextInt(i + 1);
  575. shuffleOrder[i] = shuffleOrder[j];
  576. shuffleOrder[j] = i;
  577. }
  578. if (firstIndex != null) {
  579. for (var i = 1; i < length; i++) {
  580. if (shuffleOrder[i] == firstIndex) {
  581. final v = shuffleOrder[0];
  582. shuffleOrder[0] = shuffleOrder[i];
  583. shuffleOrder[i] = v;
  584. break;
  585. }
  586. }
  587. }
  588. return shuffleOrder;
  589. }
  590. final List<AudioSourcePlayer> audioSourcePlayers;
  591. final bool useLazyPreparation;
  592. List<int> _shuffleOrder;
  593. ConcatenatingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id,
  594. this.audioSourcePlayers, this.useLazyPreparation)
  595. : _shuffleOrder = generateShuffleOrder(audioSourcePlayers.length),
  596. super(html5AudioPlayer, id);
  597. @override
  598. List<IndexedAudioSourcePlayer> get sequence =>
  599. audioSourcePlayers.expand((p) => p.sequence).toList();
  600. @override
  601. List<int> get shuffleOrder {
  602. final order = <int>[];
  603. var offset = order.length;
  604. final childOrders = <List<int>>[];
  605. for (var audioSourcePlayer in audioSourcePlayers) {
  606. final childShuffleOrder = audioSourcePlayer.shuffleOrder;
  607. childOrders.add(childShuffleOrder.map((i) => i + offset).toList());
  608. offset += childShuffleOrder.length;
  609. }
  610. for (var i = 0; i < childOrders.length; i++) {
  611. order.addAll(childOrders[_shuffleOrder[i]]);
  612. }
  613. return order;
  614. }
  615. @override
  616. int shuffle(int treeIndex, int currentIndex) {
  617. int currentChildIndex;
  618. for (var i = 0; i < audioSourcePlayers.length; i++) {
  619. final indexBefore = treeIndex;
  620. final child = audioSourcePlayers[i];
  621. treeIndex = child.shuffle(treeIndex, currentIndex);
  622. if (currentIndex >= indexBefore && currentIndex < treeIndex) {
  623. currentChildIndex = i;
  624. } else {}
  625. }
  626. // Shuffle so that the current child is first in the shuffle order
  627. _shuffleOrder =
  628. generateShuffleOrder(audioSourcePlayers.length, currentChildIndex);
  629. return treeIndex;
  630. }
  631. add(AudioSourcePlayer player) {
  632. audioSourcePlayers.add(player);
  633. _shuffleOrder.add(audioSourcePlayers.length - 1);
  634. }
  635. insert(int index, AudioSourcePlayer player) {
  636. audioSourcePlayers.insert(index, player);
  637. for (var i = 0; i < audioSourcePlayers.length; i++) {
  638. if (_shuffleOrder[i] >= index) {
  639. _shuffleOrder[i]++;
  640. }
  641. }
  642. _shuffleOrder.add(index);
  643. }
  644. addAll(List<AudioSourcePlayer> players) {
  645. audioSourcePlayers.addAll(players);
  646. _shuffleOrder.addAll(
  647. List.generate(players.length, (i) => audioSourcePlayers.length + i)
  648. .toList()
  649. ..shuffle());
  650. }
  651. insertAll(int index, List<AudioSourcePlayer> players) {
  652. audioSourcePlayers.insertAll(index, players);
  653. for (var i = 0; i < audioSourcePlayers.length; i++) {
  654. if (_shuffleOrder[i] >= index) {
  655. _shuffleOrder[i] += players.length;
  656. }
  657. }
  658. _shuffleOrder.addAll(
  659. List.generate(players.length, (i) => index + i).toList()..shuffle());
  660. }
  661. removeAt(int index) {
  662. audioSourcePlayers.removeAt(index);
  663. // 0 1 2 3
  664. // 3 2 0 1
  665. for (var i = 0; i < audioSourcePlayers.length; i++) {
  666. if (_shuffleOrder[i] > index) {
  667. _shuffleOrder[i]--;
  668. }
  669. }
  670. _shuffleOrder.removeWhere((i) => i == index);
  671. }
  672. removeRange(int start, int end) {
  673. audioSourcePlayers.removeRange(start, end);
  674. for (var i = 0; i < audioSourcePlayers.length; i++) {
  675. if (_shuffleOrder[i] >= end) {
  676. _shuffleOrder[i] -= (end - start);
  677. }
  678. }
  679. _shuffleOrder.removeWhere((i) => i >= start && i < end);
  680. }
  681. move(int currentIndex, int newIndex) {
  682. audioSourcePlayers.insert(
  683. newIndex, audioSourcePlayers.removeAt(currentIndex));
  684. }
  685. clear() {
  686. audioSourcePlayers.clear();
  687. _shuffleOrder.clear();
  688. }
  689. }
  690. class ClippingAudioSourcePlayer extends IndexedAudioSourcePlayer {
  691. final UriAudioSourcePlayer audioSourcePlayer;
  692. final Duration start;
  693. final Duration end;
  694. Completer<ClipInterruptReason> _completer;
  695. double _resumePos;
  696. Duration _duration;
  697. ClippingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id,
  698. this.audioSourcePlayer, this.start, this.end)
  699. : super(html5AudioPlayer, id);
  700. @override
  701. List<IndexedAudioSourcePlayer> get sequence => [this];
  702. @override
  703. List<int> get shuffleOrder => [0];
  704. @override
  705. Future<Duration> load() async {
  706. _resumePos = (start ?? Duration.zero).inMilliseconds / 1000.0;
  707. Duration fullDuration =
  708. await html5AudioPlayer.loadUri(audioSourcePlayer.uri);
  709. _audioElement.currentTime = _resumePos;
  710. _duration = Duration(
  711. milliseconds: min((end ?? fullDuration).inMilliseconds,
  712. fullDuration.inMilliseconds) -
  713. (start ?? Duration.zero).inMilliseconds);
  714. return _duration;
  715. }
  716. double get remaining => end.inMilliseconds / 1000 - _audioElement.currentTime;
  717. @override
  718. Future<void> play() async {
  719. _interruptPlay(ClipInterruptReason.simultaneous);
  720. _audioElement.currentTime = _resumePos;
  721. _audioElement.play();
  722. _completer = Completer<ClipInterruptReason>();
  723. ClipInterruptReason reason;
  724. while ((reason = await _completer.future) == ClipInterruptReason.seek) {
  725. _completer = Completer<ClipInterruptReason>();
  726. }
  727. if (reason == ClipInterruptReason.end) {
  728. html5AudioPlayer.onEnded();
  729. }
  730. _completer = null;
  731. }
  732. @override
  733. Future<void> pause() async {
  734. _interruptPlay(ClipInterruptReason.pause);
  735. _resumePos = _audioElement.currentTime;
  736. _audioElement.pause();
  737. }
  738. @override
  739. Future<void> seek(int position) async {
  740. _interruptPlay(ClipInterruptReason.seek);
  741. _audioElement.currentTime =
  742. _resumePos = start.inMilliseconds / 1000.0 + position / 1000.0;
  743. }
  744. @override
  745. Future<void> complete() async {
  746. _interruptPlay(ClipInterruptReason.end);
  747. }
  748. @override
  749. Future<void> timeUpdated(double seconds) async {
  750. if (end != null) {
  751. if (seconds >= end.inMilliseconds / 1000) {
  752. _interruptPlay(ClipInterruptReason.end);
  753. }
  754. }
  755. }
  756. @override
  757. Duration get duration {
  758. return _duration;
  759. }
  760. @override
  761. Duration get position {
  762. double seconds = _audioElement.currentTime;
  763. var position = Duration(milliseconds: (seconds * 1000).toInt());
  764. if (start != null) {
  765. position -= start;
  766. }
  767. if (position < Duration.zero) {
  768. position = Duration.zero;
  769. }
  770. return position;
  771. }
  772. @override
  773. Duration get bufferedPosition {
  774. if (_audioElement.buffered.length > 0) {
  775. var seconds =
  776. _audioElement.buffered.end(_audioElement.buffered.length - 1);
  777. var position = Duration(milliseconds: (seconds * 1000).toInt());
  778. if (start != null) {
  779. position -= start;
  780. }
  781. if (position < Duration.zero) {
  782. position = Duration.zero;
  783. }
  784. if (duration != null && position > duration) {
  785. position = duration;
  786. }
  787. return position;
  788. } else {
  789. return Duration.zero;
  790. }
  791. }
  792. _interruptPlay(ClipInterruptReason reason) {
  793. if (_completer?.isCompleted == false) {
  794. _completer.complete(reason);
  795. }
  796. }
  797. }
  798. enum ClipInterruptReason { end, pause, seek, simultaneous }
  799. class LoopingAudioSourcePlayer extends AudioSourcePlayer {
  800. final AudioSourcePlayer audioSourcePlayer;
  801. final int count;
  802. LoopingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id,
  803. this.audioSourcePlayer, this.count)
  804. : super(html5AudioPlayer, id);
  805. @override
  806. List<IndexedAudioSourcePlayer> get sequence =>
  807. List.generate(count, (i) => audioSourcePlayer)
  808. .expand((p) => p.sequence)
  809. .toList();
  810. @override
  811. List<int> get shuffleOrder {
  812. final order = <int>[];
  813. var offset = order.length;
  814. for (var i = 0; i < count; i++) {
  815. final childShuffleOrder = audioSourcePlayer.shuffleOrder;
  816. order.addAll(childShuffleOrder.map((i) => i + offset).toList());
  817. offset += childShuffleOrder.length;
  818. }
  819. return order;
  820. }
  821. @override
  822. int shuffle(int treeIndex, int currentIndex) {
  823. for (var i = 0; i < count; i++) {
  824. treeIndex = audioSourcePlayer.shuffle(treeIndex, currentIndex);
  825. }
  826. return treeIndex;
  827. }
  828. }