import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:rxdart/rxdart.dart'; /// An object to manage playing audio from a URL, a locale file or an asset. /// /// ``` /// final player = AudioPlayer(); /// await player.setUrl('https://foo.com/bar.mp3'); /// player.play(); /// await player.pause(); /// await player.play(untilPosition: Duration(minutes: 1)); /// await player.stop() /// await player.setUrl('https://foo.com/baz.mp3'); /// await player.seek(Duration(minutes: 5)); /// player.play(); /// await player.stop(); /// await player.dispose(); /// ``` /// /// You must call [dispose] to release the resources used by this player, /// including any temporary files created to cache assets. /// /// The [AudioPlayer] instance transitions through different states as follows: /// /// * [AudioPlaybackState.none]: immediately after instantiation. /// * [AudioPlaybackState.stopped]: eventually after [setUrl], [setFilePath] or /// [setAsset] completes, and immediately after [stop]. /// * [AudioPlaybackState.paused]: after [pause] and after reaching the end of /// the requested [play] segment. /// * [AudioPlaybackState.playing]: after [play] and after sufficiently /// buffering during normal playback. /// * [AudioPlaybackState.buffering]: immediately after a seek request and /// during normal playback when the next buffer is not ready to be played. /// * [AudioPlaybackState.connecting]: immediately after [setUrl], /// [setFilePath] and [setAsset] while waiting for the media to load. /// * [AudioPlaybackState.completed]: immediately after playback reaches the /// end of the media. /// /// Additionally, after a [seek] request completes, the state will return to /// whatever state the player was in prior to the seek request. class AudioPlayer { static final _mainChannel = MethodChannel('com.ryanheise.just_audio.methods'); static Future _init(int id) async { await _mainChannel.invokeMethod('init', ['$id']); return MethodChannel('com.ryanheise.just_audio.methods.$id'); } final Future _channel; final int _id; Future _durationFuture; final _durationSubject = BehaviorSubject(); AudioPlaybackEvent _audioPlaybackEvent; Stream _eventChannelStream; StreamSubscription _eventChannelStreamSubscription; final _playbackEventSubject = BehaviorSubject(); final _playbackStateSubject = BehaviorSubject(); double _volume = 1.0; double _speed = 1.0; /// Creates an [AudioPlayer]. factory AudioPlayer() => AudioPlayer._internal(DateTime.now().microsecondsSinceEpoch); AudioPlayer._internal(this._id) : _channel = _init(_id) { _eventChannelStream = EventChannel('com.ryanheise.just_audio.events.$_id') .receiveBroadcastStream() .map((data) => _audioPlaybackEvent = AudioPlaybackEvent( state: AudioPlaybackState.values[data[0]], updatePosition: Duration(milliseconds: data[1]), updateTime: Duration(milliseconds: data[2]), speed: _speed, )); _eventChannelStreamSubscription = _eventChannelStream.listen(_playbackEventSubject.add); _playbackStateSubject .addStream(playbackEventStream.map((state) => state.state).distinct()); } /// The duration of any media set via [setUrl], [setFilePath] or [setAsset], /// or null otherwise. Future get durationFuture => _durationFuture; /// The duration of any media set via [setUrl], [setFilePath] or [setAsset]. Stream get durationStream => _durationSubject.stream; /// The latest [AudioPlaybackEvent]. AudioPlaybackEvent get playbackEvent => _audioPlaybackEvent; /// A stream of [AudioPlaybackEvent]s. Stream get playbackEventStream => _playbackEventSubject.stream; /// The current [AudioPlaybackState]. AudioPlaybackState get playbackState => _audioPlaybackEvent.state; /// A stream of [AudioPlaybackState]s. Stream get playbackStateStream => _playbackStateSubject.stream; /// A stream periodically tracking the current position of this player. Stream getPositionStream( [final Duration period = const Duration(milliseconds: 200)]) => Rx.combineLatest2( playbackEventStream, // TODO: emit periodically only in playing state. Stream.periodic(period), (state, _) => state.position); /// The current volume of the player. double get volume => _volume; /// The current speed of the player. double get speed => _speed; /// Loads audio media from a URL and returns the duration of that audio. It /// is legal to invoke this method only from one of the following states: /// /// * [AudioPlaybackState.none] /// * [AudioPlaybackState.stopped] /// * [AudioPlaybackState.completed] Future setUrl(final String url) async { _durationFuture = _invokeMethod('setUrl', [url]).then((ms) => Duration(milliseconds: ms)); final duration = await _durationFuture; _durationSubject.add(duration); return duration; } /// Loads audio media from a file and returns the duration of that audio. It /// is legal to invoke this method only from one of the following states: /// /// * [AudioPlaybackState.none] /// * [AudioPlaybackState.stopped] /// * [AudioPlaybackState.completed] Future setFilePath(final String filePath) => setUrl('file://$filePath'); /// Loads audio media from an asset and returns the duration of that audio. /// It is legal to invoke this method only from one of the following states: /// /// * [AudioPlaybackState.none] /// * [AudioPlaybackState.stopped] /// * [AudioPlaybackState.completed] Future setAsset(final String assetPath) async { final file = await _cacheFile; if (!file.existsSync()) { await file.create(recursive: true); } await file .writeAsBytes((await rootBundle.load(assetPath)).buffer.asUint8List()); return await setFilePath(file.path); } Future get _cacheFile async => File(p.join( (await getTemporaryDirectory()).path, 'just_audio_asset_cache', '$_id')); /// Plays the currently loaded media from the current position, until the /// given position if specified. The [Future] returned by this method /// completes when playback completes or is paused or stopped. It is legal to /// invoke this method only from one of the following states: /// /// * [AudioPlaybackState.stopped] /// * [AudioPlaybackState.completed] /// * [AudioPlaybackState.paused] Future play({final Duration untilPosition}) async { StreamSubscription subscription; Completer completer = Completer(); subscription = playbackStateStream .skip(1) .where((state) => state == AudioPlaybackState.paused || state == AudioPlaybackState.stopped) .listen((state) { subscription.cancel(); completer.complete(); }); await _invokeMethod('play', [untilPosition?.inMilliseconds]); await completer.future; } /// Pauses the currently playing media. It is legal to invoke this method /// only from the following states: /// /// * [AudioPlaybackState.playing] /// * [AudioPlaybackState.buffering] Future pause() async { await _invokeMethod('pause'); } /// Stops the currently playing media such that the next [play] invocation /// will start from position 0. It is legal to invoke this method only from /// the following states: /// /// * [AudioPlaybackState.playing] /// * [AudioPlaybackState.paused] /// * [AudioPlaybackState.stopped] /// * [AudioPlaybackState.completed] Future stop() async { await _invokeMethod('stop'); } /// Sets the volume of this player, where 1.0 is normal volume. Future setVolume(final double volume) async { _volume = volume; await _invokeMethod('setVolume', [volume]); } /// Sets the playback speed of this player, where 1.0 is normal speed. Future setSpeed(final double speed) async { _speed = speed; await _invokeMethod('setSpeed', [speed]); } /// Seeks to a particular position. It is legal to invoke this method from /// any state except for [AudioPlaybackState.none] and /// [AudioPlaybackState.connecting]. Future seek(final Duration position) async { await _invokeMethod('seek', [position.inMilliseconds]); } /// Release all resources associated with this player. You must invoke this /// after you are done with the player. It is legal to invoke this method /// only from the following states: /// /// * [AudioPlaybackState.stopped] /// * [AudioPlaybackState.completed] /// * [AudioPlaybackState.none] Future dispose() async { if ((await _cacheFile).existsSync()) { (await _cacheFile).deleteSync(); } await _invokeMethod('dispose'); await _durationSubject.close(); await _eventChannelStreamSubscription.cancel(); await _playbackEventSubject.close(); } Future _invokeMethod(String method, [dynamic args]) async => (await _channel).invokeMethod(method, args); } /// Encapsulates the playback state and current position of the player. class AudioPlaybackEvent { /// The current playback state. final AudioPlaybackState state; /// When the last time a position discontinuity happened, as measured in time /// since the epoch. final Duration updateTime; /// The position at [updateTime]. final Duration updatePosition; /// The playback speed. final double speed; AudioPlaybackEvent({ @required this.state, @required this.updateTime, @required this.updatePosition, @required this.speed, }); /// The current position of the player. Duration get position => state == AudioPlaybackState.playing ? updatePosition + (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch) - updateTime) * speed : updatePosition; @override String toString() => "{state=$state, updateTime=$updateTime, updatePosition=$updatePosition, speed=$speed}"; } /// Enumerates the different playback states of a player. enum AudioPlaybackState { none, stopped, paused, playing, buffering, connecting, completed, }