import 'dart:async'; import 'dart:html'; import 'dart:math'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:just_audio_platform_interface/just_audio_platform_interface.dart'; final Random _random = Random(); class JustAudioPlugin extends JustAudioPlatform { final Map players = {}; static void registerWith(Registrar registrar) { JustAudioPlatform.instance = JustAudioPlugin(); } Future init(InitRequest request) async { final player = Html5AudioPlayer(id: request.id); players[request.id] = player; return player; } Future disposePlayer( DisposePlayerRequest request) async { await players[request.id]?.release(); return DisposePlayerResponse(); } } abstract class JustAudioPlayer extends AudioPlayerPlatform { final eventController = StreamController(); ProcessingStateMessage _processingState = ProcessingStateMessage.none; bool _playing = false; int _index; JustAudioPlayer({@required String id}) : super(id); @mustCallSuper Future release() async { eventController.close(); } Duration getCurrentPosition(); Duration getBufferedPosition(); Duration getDuration(); broadcastPlaybackEvent() { var updateTime = DateTime.now(); eventController.add(PlaybackEventMessage( processingState: _processingState, updatePosition: getCurrentPosition(), updateTime: updateTime, bufferedPosition: getBufferedPosition(), // TODO: Icy Metadata icyMetadata: null, duration: getDuration(), currentIndex: _index, androidAudioSessionId: null, )); } transition(ProcessingStateMessage processingState) { _processingState = processingState; broadcastPlaybackEvent(); } } class Html5AudioPlayer extends JustAudioPlayer { AudioElement _audioElement = AudioElement(); Completer _durationCompleter; AudioSourcePlayer _audioSourcePlayer; LoopModeMessage _loopMode = LoopModeMessage.off; bool _shuffleModeEnabled = false; final Map _audioSourcePlayers = {}; Html5AudioPlayer({@required String id}) : super(id: id) { _audioElement.addEventListener('durationchange', (event) { _durationCompleter?.complete(); broadcastPlaybackEvent(); }); _audioElement.addEventListener('error', (event) { _durationCompleter?.completeError(_audioElement.error); }); _audioElement.addEventListener('ended', (event) async { _currentAudioSourcePlayer.complete(); }); _audioElement.addEventListener('timeupdate', (event) { _currentAudioSourcePlayer.timeUpdated(_audioElement.currentTime); }); _audioElement.addEventListener('loadstart', (event) { transition(ProcessingStateMessage.buffering); }); _audioElement.addEventListener('waiting', (event) { transition(ProcessingStateMessage.buffering); }); _audioElement.addEventListener('stalled', (event) { transition(ProcessingStateMessage.buffering); }); _audioElement.addEventListener('canplaythrough', (event) { transition(ProcessingStateMessage.ready); }); _audioElement.addEventListener('progress', (event) { broadcastPlaybackEvent(); }); } List get order { final sequence = _audioSourcePlayer.sequence; List order = List(sequence.length); if (_shuffleModeEnabled) { order = _audioSourcePlayer.shuffleOrder; } else { for (var i = 0; i < order.length; i++) { order[i] = i; } } return order; } List getInv(List order) { List orderInv = List(order.length); for (var i = 0; i < order.length; i++) { orderInv[order[i]] = i; } return orderInv; } onEnded() async { if (_loopMode == LoopModeMessage.one) { await _seek(0, null); _play(); } else { final order = this.order; final orderInv = getInv(order); if (orderInv[_index] + 1 < order.length) { // move to next item _index = order[orderInv[_index] + 1]; await _currentAudioSourcePlayer.load(); // Should always be true... if (_playing) { _play(); } } else { // reached end of playlist if (_loopMode == LoopModeMessage.all) { // Loop back to the beginning if (order.length == 1) { await _seek(0, null); _play(); } else { _index = order[0]; await _currentAudioSourcePlayer.load(); // Should always be true... if (_playing) { _play(); } } } else { transition(ProcessingStateMessage.completed); } } } } // TODO: Improve efficiency. IndexedAudioSourcePlayer get _currentAudioSourcePlayer => _audioSourcePlayer != null && _index < _audioSourcePlayer.sequence.length ? _audioSourcePlayer.sequence[_index] : null; @override Stream get playbackEventMessageStream => eventController.stream; @override Future load(LoadRequest request) async { _currentAudioSourcePlayer?.pause(); _audioSourcePlayer = getAudioSource(request.audioSourceMessage); _index = request.initialIndex ?? 0; if (_shuffleModeEnabled) { _audioSourcePlayer?.shuffle(0, _index); } final duration = await _currentAudioSourcePlayer.load(); if (request.initialPosition != null) { await _currentAudioSourcePlayer .seek(request.initialPosition.inMilliseconds); } if (_playing) { _currentAudioSourcePlayer.play(); } return LoadResponse(duration: duration); } Future loadUri(final Uri uri) async { transition(ProcessingStateMessage.loading); final src = uri.toString(); if (src != _audioElement.src) { _durationCompleter = Completer(); _audioElement.src = src; _audioElement.preload = 'auto'; _audioElement.load(); try { await _durationCompleter.future; } on MediaError catch (e) { throw PlatformException( code: "${e.code}", message: "Failed to load URL"); } finally { _durationCompleter = null; } } transition(ProcessingStateMessage.ready); final seconds = _audioElement.duration; return seconds.isFinite ? Duration(milliseconds: (seconds * 1000).toInt()) : null; } @override Future play(PlayRequest request) async { await _play(); return PlayResponse(); } Future _play() async { _playing = true; await _currentAudioSourcePlayer.play(); } @override Future pause(PauseRequest request) async { _playing = false; _currentAudioSourcePlayer.pause(); return PauseResponse(); } @override Future setVolume(SetVolumeRequest request) async { _audioElement.volume = request.volume; return SetVolumeResponse(); } @override Future setSpeed(SetSpeedRequest request) async { _audioElement.playbackRate = request.speed; return SetSpeedResponse(); } @override Future setLoopMode(SetLoopModeRequest request) async { _loopMode = request.loopMode; return SetLoopModeResponse(); } @override Future setShuffleMode( SetShuffleModeRequest request) async { _shuffleModeEnabled = request.shuffleMode == ShuffleModeMessage.all; if (_shuffleModeEnabled) { _audioSourcePlayer?.shuffle(0, _index); } return SetShuffleModeResponse(); } @override Future seek(SeekRequest request) async { await _seek(request.position.inMilliseconds, request.index); return SeekResponse(); } Future _seek(int position, int newIndex) async { int index = newIndex ?? _index; if (index != _index) { _currentAudioSourcePlayer.pause(); _index = index; await _currentAudioSourcePlayer.load(); await _currentAudioSourcePlayer.seek(position); if (_playing) { _currentAudioSourcePlayer.play(); } } else { await _currentAudioSourcePlayer.seek(position); } } ConcatenatingAudioSourcePlayer _concatenating(String playerId) => _audioSourcePlayers[playerId] as ConcatenatingAudioSourcePlayer; @override Future concatenatingInsertAll( ConcatenatingInsertAllRequest request) async { _concatenating(request.id) .insertAll(request.index, getAudioSources(request.children)); if (request.index <= _index) { _index += request.children.length; } return ConcatenatingInsertAllResponse(); } @override Future concatenatingRemoveRange( ConcatenatingRemoveRangeRequest request) async { if (_index >= request.startIndex && _index < request.endIndex && _playing) { // Pause if removing current item _currentAudioSourcePlayer.pause(); } _concatenating(request.id) .removeRange(request.startIndex, request.endIndex); if (_index >= request.startIndex && _index < request.endIndex) { // Skip backward if there's nothing after this if (request.startIndex >= _audioSourcePlayer.sequence.length) { _index = request.startIndex - 1; } else { _index = request.startIndex; } // Resume playback at the new item (if it exists) if (_playing && _currentAudioSourcePlayer != null) { await _currentAudioSourcePlayer.load(); _currentAudioSourcePlayer.play(); } } else if (request.endIndex <= _index) { // Reflect that the current item has shifted its position _index -= (request.endIndex - request.startIndex); } return ConcatenatingRemoveRangeResponse(); } @override Future concatenatingMove( ConcatenatingMoveRequest request) async { _concatenating(request.id).move(request.currentIndex, request.newIndex); if (request.currentIndex == _index) { _index = request.newIndex; } else if (request.currentIndex < _index && request.newIndex >= _index) { _index--; } else if (request.currentIndex > _index && request.newIndex <= _index) { _index++; } return ConcatenatingMoveResponse(); } @override Future setAndroidAudioAttributes( SetAndroidAudioAttributesRequest request) async { return SetAndroidAudioAttributesResponse(); } @override Future setAutomaticallyWaitsToMinimizeStalling( SetAutomaticallyWaitsToMinimizeStallingRequest request) async { return SetAutomaticallyWaitsToMinimizeStallingResponse(); } @override Duration getCurrentPosition() => _currentAudioSourcePlayer?.position; @override Duration getBufferedPosition() => _currentAudioSourcePlayer?.bufferedPosition; @override Duration getDuration() => _currentAudioSourcePlayer?.duration; @override Future release() async { _currentAudioSourcePlayer?.pause(); _audioElement.removeAttribute('src'); _audioElement.load(); transition(ProcessingStateMessage.none); return await super.release(); } List getAudioSources(List messages) => messages.map((message) => getAudioSource(message)).toList(); AudioSourcePlayer getAudioSource(AudioSourceMessage audioSourceMessage) { final String id = audioSourceMessage.id; var audioSourcePlayer = _audioSourcePlayers[id]; if (audioSourcePlayer == null) { audioSourcePlayer = decodeAudioSource(audioSourceMessage); _audioSourcePlayers[id] = audioSourcePlayer; } return audioSourcePlayer; } AudioSourcePlayer decodeAudioSource(AudioSourceMessage audioSourceMessage) { if (audioSourceMessage is ProgressiveAudioSourceMessage) { return ProgressiveAudioSourcePlayer(this, audioSourceMessage.id, Uri.parse(audioSourceMessage.uri), audioSourceMessage.headers); } else if (audioSourceMessage is DashAudioSourceMessage) { return DashAudioSourcePlayer(this, audioSourceMessage.id, Uri.parse(audioSourceMessage.uri), audioSourceMessage.headers); } else if (audioSourceMessage is HlsAudioSourceMessage) { return HlsAudioSourcePlayer(this, audioSourceMessage.id, Uri.parse(audioSourceMessage.uri), audioSourceMessage.headers); } else if (audioSourceMessage is ConcatenatingAudioSourceMessage) { return ConcatenatingAudioSourcePlayer( this, audioSourceMessage.id, getAudioSources(audioSourceMessage.children), audioSourceMessage.useLazyPreparation); } else if (audioSourceMessage is ClippingAudioSourceMessage) { return ClippingAudioSourcePlayer( this, audioSourceMessage.id, getAudioSource(audioSourceMessage.child), audioSourceMessage.start, audioSourceMessage.end); } else if (audioSourceMessage is LoopingAudioSourceMessage) { return LoopingAudioSourcePlayer(this, audioSourceMessage.id, getAudioSource(audioSourceMessage.child), audioSourceMessage.count); } else { throw Exception("Unknown AudioSource type: $audioSourceMessage"); } } } abstract class AudioSourcePlayer { Html5AudioPlayer html5AudioPlayer; final String id; AudioSourcePlayer(this.html5AudioPlayer, this.id); List get sequence; List get shuffleOrder; int shuffle(int treeIndex, int currentIndex); } abstract class IndexedAudioSourcePlayer extends AudioSourcePlayer { IndexedAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id) : super(html5AudioPlayer, id); Future load(); Future play(); Future pause(); Future seek(int position); Future complete(); Future timeUpdated(double seconds) async {} Duration get duration; Duration get position; Duration get bufferedPosition; AudioElement get _audioElement => html5AudioPlayer._audioElement; @override int shuffle(int treeIndex, int currentIndex) => treeIndex + 1; @override String toString() => "${this.runtimeType}"; } abstract class UriAudioSourcePlayer extends IndexedAudioSourcePlayer { final Uri uri; final Map headers; double _resumePos; Duration _duration; Completer _completer; UriAudioSourcePlayer( Html5AudioPlayer html5AudioPlayer, String id, this.uri, this.headers) : super(html5AudioPlayer, id); @override List get sequence => [this]; @override List get shuffleOrder => [0]; @override Future load() async { _resumePos = 0.0; return _duration = await html5AudioPlayer.loadUri(uri); } @override Future play() async { _audioElement.currentTime = _resumePos; _audioElement.play(); _completer = Completer(); await _completer.future; _completer = null; } @override Future pause() async { _resumePos = _audioElement.currentTime; _audioElement.pause(); _interruptPlay(); } @override Future seek(int position) async { _audioElement.currentTime = _resumePos = position / 1000.0; } @override Future complete() async { _interruptPlay(); html5AudioPlayer.onEnded(); } _interruptPlay() { if (_completer?.isCompleted == false) { _completer.complete(); } } @override Duration get duration { return _duration; //final seconds = _audioElement.duration; //return seconds.isFinite // ? Duration(milliseconds: (seconds * 1000).toInt()) // : null; } @override Duration get position { double seconds = _audioElement.currentTime; return Duration(milliseconds: (seconds * 1000).toInt()); } @override Duration get bufferedPosition { if (_audioElement.buffered.length > 0) { return Duration( milliseconds: (_audioElement.buffered.end(_audioElement.buffered.length - 1) * 1000) .toInt()); } else { return Duration.zero; } } } class ProgressiveAudioSourcePlayer extends UriAudioSourcePlayer { ProgressiveAudioSourcePlayer( Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers) : super(html5AudioPlayer, id, uri, headers); } class DashAudioSourcePlayer extends UriAudioSourcePlayer { DashAudioSourcePlayer( Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers) : super(html5AudioPlayer, id, uri, headers); } class HlsAudioSourcePlayer extends UriAudioSourcePlayer { HlsAudioSourcePlayer( Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers) : super(html5AudioPlayer, id, uri, headers); } class ConcatenatingAudioSourcePlayer extends AudioSourcePlayer { static List generateShuffleOrder(int length, [int firstIndex]) { final shuffleOrder = List(length); for (var i = 0; i < length; i++) { final j = _random.nextInt(i + 1); shuffleOrder[i] = shuffleOrder[j]; shuffleOrder[j] = i; } if (firstIndex != null) { for (var i = 1; i < length; i++) { if (shuffleOrder[i] == firstIndex) { final v = shuffleOrder[0]; shuffleOrder[0] = shuffleOrder[i]; shuffleOrder[i] = v; break; } } } return shuffleOrder; } final List audioSourcePlayers; final bool useLazyPreparation; List _shuffleOrder; ConcatenatingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id, this.audioSourcePlayers, this.useLazyPreparation) : _shuffleOrder = generateShuffleOrder(audioSourcePlayers.length), super(html5AudioPlayer, id); @override List get sequence => audioSourcePlayers.expand((p) => p.sequence).toList(); @override List get shuffleOrder { final order = []; var offset = order.length; final childOrders = >[]; for (var audioSourcePlayer in audioSourcePlayers) { final childShuffleOrder = audioSourcePlayer.shuffleOrder; childOrders.add(childShuffleOrder.map((i) => i + offset).toList()); offset += childShuffleOrder.length; } for (var i = 0; i < childOrders.length; i++) { order.addAll(childOrders[_shuffleOrder[i]]); } return order; } @override int shuffle(int treeIndex, int currentIndex) { int currentChildIndex; for (var i = 0; i < audioSourcePlayers.length; i++) { final indexBefore = treeIndex; final child = audioSourcePlayers[i]; treeIndex = child.shuffle(treeIndex, currentIndex); if (currentIndex >= indexBefore && currentIndex < treeIndex) { currentChildIndex = i; } else {} } // Shuffle so that the current child is first in the shuffle order _shuffleOrder = generateShuffleOrder(audioSourcePlayers.length, currentChildIndex); return treeIndex; } insertAll(int index, List players) { audioSourcePlayers.insertAll(index, players); for (var i = 0; i < audioSourcePlayers.length; i++) { if (_shuffleOrder[i] >= index) { _shuffleOrder[i] += players.length; } } _shuffleOrder.addAll( List.generate(players.length, (i) => index + i).toList()..shuffle()); } removeRange(int start, int end) { audioSourcePlayers.removeRange(start, end); for (var i = 0; i < audioSourcePlayers.length; i++) { if (_shuffleOrder[i] >= end) { _shuffleOrder[i] -= (end - start); } } _shuffleOrder.removeWhere((i) => i >= start && i < end); } move(int currentIndex, int newIndex) { audioSourcePlayers.insert( newIndex, audioSourcePlayers.removeAt(currentIndex)); } } class ClippingAudioSourcePlayer extends IndexedAudioSourcePlayer { final UriAudioSourcePlayer audioSourcePlayer; final Duration start; final Duration end; Completer _completer; double _resumePos; Duration _duration; ClippingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id, this.audioSourcePlayer, this.start, this.end) : super(html5AudioPlayer, id); @override List get sequence => [this]; @override List get shuffleOrder => [0]; @override Future load() async { _resumePos = (start ?? Duration.zero).inMilliseconds / 1000.0; Duration fullDuration = await html5AudioPlayer.loadUri(audioSourcePlayer.uri); _audioElement.currentTime = _resumePos; _duration = Duration( milliseconds: min((end ?? fullDuration).inMilliseconds, fullDuration.inMilliseconds) - (start ?? Duration.zero).inMilliseconds); return _duration; } double get remaining => end.inMilliseconds / 1000 - _audioElement.currentTime; @override Future play() async { _interruptPlay(ClipInterruptReason.simultaneous); _audioElement.currentTime = _resumePos; _audioElement.play(); _completer = Completer(); ClipInterruptReason reason; while ((reason = await _completer.future) == ClipInterruptReason.seek) { _completer = Completer(); } if (reason == ClipInterruptReason.end) { html5AudioPlayer.onEnded(); } _completer = null; } @override Future pause() async { _interruptPlay(ClipInterruptReason.pause); _resumePos = _audioElement.currentTime; _audioElement.pause(); } @override Future seek(int position) async { _interruptPlay(ClipInterruptReason.seek); _audioElement.currentTime = _resumePos = start.inMilliseconds / 1000.0 + position / 1000.0; } @override Future complete() async { _interruptPlay(ClipInterruptReason.end); } @override Future timeUpdated(double seconds) async { if (end != null) { if (seconds >= end.inMilliseconds / 1000) { _interruptPlay(ClipInterruptReason.end); } } } @override Duration get duration { return _duration; } @override Duration get position { double seconds = _audioElement.currentTime; var position = Duration(milliseconds: (seconds * 1000).toInt()); if (start != null) { position -= start; } if (position < Duration.zero) { position = Duration.zero; } return position; } @override Duration get bufferedPosition { if (_audioElement.buffered.length > 0) { var seconds = _audioElement.buffered.end(_audioElement.buffered.length - 1); var position = Duration(milliseconds: (seconds * 1000).toInt()); if (start != null) { position -= start; } if (position < Duration.zero) { position = Duration.zero; } if (duration != null && position > duration) { position = duration; } return position; } else { return Duration.zero; } } _interruptPlay(ClipInterruptReason reason) { if (_completer?.isCompleted == false) { _completer.complete(reason); } } } enum ClipInterruptReason { end, pause, seek, simultaneous } class LoopingAudioSourcePlayer extends AudioSourcePlayer { final AudioSourcePlayer audioSourcePlayer; final int count; LoopingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id, this.audioSourcePlayer, this.count) : super(html5AudioPlayer, id); @override List get sequence => List.generate(count, (i) => audioSourcePlayer) .expand((p) => p.sequence) .toList(); @override List get shuffleOrder { final order = []; var offset = order.length; for (var i = 0; i < count; i++) { final childShuffleOrder = audioSourcePlayer.shuffleOrder; order.addAll(childShuffleOrder.map((i) => i + offset).toList()); offset += childShuffleOrder.length; } return order; } @override int shuffle(int treeIndex, int currentIndex) { for (var i = 0; i < count; i++) { treeIndex = audioSourcePlayer.shuffle(treeIndex, currentIndex); } return treeIndex; } }