From 0574c041c54f03f5732897784302a9fc9685265e Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 28 Nov 2019 16:16:54 +1100 Subject: [PATCH] Dart/Android implementation --- CHANGELOG.md | 2 +- README.md | 16 +- .../ryanheise/audio_player/AudioPlayer.java | 259 ++++++++++++++++++ .../audio_player/AudioPlayerPlugin.java | 25 +- example/lib/main.dart | 142 ++++++++-- example/pubspec.lock | 22 ++ example/pubspec.yaml | 3 - lib/audio_player.dart | 237 +++++++++++++++- pubspec.lock | 24 +- pubspec.yaml | 9 +- 10 files changed, 688 insertions(+), 51 deletions(-) create mode 100644 android/src/main/java/com/ryanheise/audio_player/AudioPlayer.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..a207e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ ## 0.0.1 -* TODO: Describe initial release. +* Initial release with Android implementation first. diff --git a/README.md b/README.md index 3540612..c842fac 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ # audio_player -A new flutter plugin project. +A new Flutter audio player plugin designed to support background playback with [audio_service](https://pub.dev/packages/audio_service) -## Getting Started +## Features -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. +* Plays audio from streams, files and assets. +* Broadcasts state changes helpful in streaming apps such as `buffering` and `connecting` in addition to the typical `playing`, `paused` and `stopped` states. +* Control audio playback via standard operations: play, pause, stop, setVolume, seek. +* Compatible with [audio_service](https://pub.dev/packages/audio_service) to support full background playback, queue management, and controlling playback from the lock screen, notifications and headset buttons. -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +The initial release is for Android. iOS is the next priority. diff --git a/android/src/main/java/com/ryanheise/audio_player/AudioPlayer.java b/android/src/main/java/com/ryanheise/audio_player/AudioPlayer.java new file mode 100644 index 0000000..d7514b9 --- /dev/null +++ b/android/src/main/java/com/ryanheise/audio_player/AudioPlayer.java @@ -0,0 +1,259 @@ +package com.ryanheise.audio_player; + +import android.media.MediaPlayer; +import android.media.MediaTimestamp; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.EventChannel.EventSink; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import java.util.List; +import java.io.IOException; +import android.os.Handler; +import java.util.ArrayList; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +public class AudioPlayer implements MethodCallHandler, MediaPlayer.OnCompletionListener { + private final Registrar registrar; + private final MethodChannel methodChannel; + private final EventChannel eventChannel; + private EventSink eventSink; + private final Handler handler = new Handler(); + private Runnable endDetector; + private final Runnable positionObserver = new Runnable() { + @Override + public void run() { + if (state != PlaybackState.playing && state != PlaybackState.buffering) + return; + + if (eventSink != null) { + checkForDiscontinuity(); + } + handler.postDelayed(this, 200); + } + }; + + private final long id; + private final MediaPlayer player; + private PlaybackState state; + private PlaybackState stateBeforeSeek; + private long updateTime; + private int updatePosition; + private Integer seekPos; + + public AudioPlayer(final Registrar registrar, final long id) { + this.registrar = registrar; + this.id = id; + methodChannel = new MethodChannel(registrar.messenger(), "com.ryanheise.audio_player.methods." + id); + methodChannel.setMethodCallHandler(this); + eventChannel = new EventChannel(registrar.messenger(), "com.ryanheise.audio_player.events." + id); + eventChannel.setStreamHandler(new EventChannel.StreamHandler() { + @Override + public void onListen(final Object arguments, final EventSink eventSink) { + AudioPlayer.this.eventSink = eventSink; + } + + @Override + public void onCancel(final Object arguments) { + eventSink = null; + } + }); + state = PlaybackState.none; + player = new MediaPlayer(); + player.setOnCompletionListener(this); + } + + private void checkForDiscontinuity() { + // TODO: Consider using player.setOnMediaTimeDiscontinuityListener() + // when available in SDK. (Added in API level 28) + final long now = System.currentTimeMillis(); + final int position = getCurrentPosition(); + final long timeSinceLastUpdate = now - updateTime; + final long expectedPosition = updatePosition + timeSinceLastUpdate; + final long drift = position - expectedPosition; + // Update if we've drifted or just started observing + if (updateTime == 0L) { + broadcastPlayerState(); + } else if (drift < -100) { + System.out.println("time discontinuity detected: " + drift); + setPlaybackState(PlaybackState.buffering); + } else if (state == PlaybackState.buffering) { + setPlaybackState(PlaybackState.playing); + } + } + + @Override + public void onCompletion(final MediaPlayer mp) { + setPlaybackState(PlaybackState.stopped); + } + + @Override + public void onMethodCall(final MethodCall call, final Result result) { + final List args = (List)call.arguments; + try { + switch (call.method) { + case "setUrl": + setUrl((String)args.get(0), result); + break; + case "play": + play((Integer)args.get(0)); + result.success(null); + break; + case "pause": + pause(); + result.success(null); + break; + case "stop": + stop(); + result.success(null); + break; + case "setVolume": + setVolume((Double)args.get(0)); + result.success(null); + break; + case "seek": + seek((Integer)args.get(0), result); + break; + case "dispose": + dispose(); + result.success(null); + break; + default: + result.notImplemented(); + break; + } + } catch (IllegalStateException e) { + e.printStackTrace(); + result.error("Illegal state", null, null); + } catch (Exception e) { + e.printStackTrace(); + result.error("Error", null, null); + } + } + + private void broadcastPlayerState() { + final ArrayList event = new ArrayList(); + // state + event.add(state.ordinal()); + // updatePosition + event.add(updatePosition = getCurrentPosition()); + // updateTime + event.add(updateTime = System.currentTimeMillis()); + eventSink.success(event); + } + + private int getCurrentPosition() { + if (state == PlaybackState.none || state == PlaybackState.connecting) { + return 0; + } else if (seekPos != null) { + return seekPos; + } else { + return player.getCurrentPosition(); + } + } + + private void setPlaybackState(final PlaybackState state) { + final PlaybackState oldState = this.state; + this.state = state; + if (oldState != PlaybackState.playing && state == PlaybackState.playing) { + startObservingPosition(); + } + broadcastPlayerState(); + } + + public void setUrl(final String url, final Result result) throws IOException { + setPlaybackState(PlaybackState.connecting); + player.reset(); + player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(final MediaPlayer mp) { + setPlaybackState(PlaybackState.stopped); + result.success(mp.getDuration()); + } + }); + player.setDataSource(url); + player.prepareAsync(); + } + + public void play(final Integer untilPosition) { + // TODO: dynamically adjust the lag. + final int lag = 6; + final int start = getCurrentPosition(); + if (untilPosition != null && untilPosition <= start) { + return; + } + player.start(); + setPlaybackState(PlaybackState.playing); + if (endDetector != null) { + handler.removeCallbacks(endDetector); + } + if (untilPosition != null) { + final int duration = Math.max(0, untilPosition - start - lag); + handler.postDelayed(new Runnable() { + @Override + public void run() { + final int position = getCurrentPosition(); + if (position > untilPosition - 20) { + pause(); + } else { + final int duration = Math.max(0, untilPosition - position - lag); + handler.postDelayed(this, duration); + } + } + }, duration); + } + } + + public void pause() { + player.pause(); + setPlaybackState(PlaybackState.paused); + } + + public void stop() { + player.pause(); + player.seekTo(0); + setPlaybackState(PlaybackState.stopped); + } + + public void setVolume(final double volume) { + player.setVolume((float)volume, (float)volume); + } + + public void seek(final int position, final Result result) { + stateBeforeSeek = state; + seekPos = position; + handler.removeCallbacks(positionObserver); + setPlaybackState(PlaybackState.buffering); + player.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(final MediaPlayer mp) { + seekPos = null; + setPlaybackState(stateBeforeSeek); + stateBeforeSeek = null; + result.success(null); + player.setOnSeekCompleteListener(null); + } + }); + player.seekTo(position); + } + + public void dispose() { + player.release(); + setPlaybackState(PlaybackState.none); + } + + private void startObservingPosition() { + handler.removeCallbacks(positionObserver); + handler.post(positionObserver); + } + + enum PlaybackState { + none, + stopped, + paused, + playing, + buffering, + connecting + } +} diff --git a/android/src/main/java/com/ryanheise/audio_player/AudioPlayerPlugin.java b/android/src/main/java/com/ryanheise/audio_player/AudioPlayerPlugin.java index 0572604..7c5023a 100644 --- a/android/src/main/java/com/ryanheise/audio_player/AudioPlayerPlugin.java +++ b/android/src/main/java/com/ryanheise/audio_player/AudioPlayerPlugin.java @@ -10,16 +10,27 @@ import io.flutter.plugin.common.PluginRegistry.Registrar; public class AudioPlayerPlugin implements MethodCallHandler { /** Plugin registration. */ public static void registerWith(Registrar registrar) { - final MethodChannel channel = new MethodChannel(registrar.messenger(), "audio_player"); - channel.setMethodCallHandler(new AudioPlayerPlugin()); + final MethodChannel channel = new MethodChannel(registrar.messenger(), "com.ryanheise.audio_player.methods"); + channel.setMethodCallHandler(new AudioPlayerPlugin(registrar)); } + private Registrar registrar; + + public AudioPlayerPlugin(Registrar registrar) { + this.registrar = registrar; + } + @Override public void onMethodCall(MethodCall call, Result result) { - if (call.method.equals("getPlatformVersion")) { - result.success("Android " + android.os.Build.VERSION.RELEASE); - } else { - result.notImplemented(); - } + switch (call.method) { + case "init": + long id = (Long)call.arguments; + new AudioPlayer(registrar, id); + result.success(null); + break; + default: + result.notImplemented(); + break; + } } } diff --git a/example/lib/main.dart b/example/lib/main.dart index d0eaae6..6d2ea54 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'dart:async'; @@ -12,32 +14,19 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _platformVersion = 'Unknown'; + final AudioPlayer _player = AudioPlayer(); @override void initState() { super.initState(); - initPlatformState(); + _player.setUrl( + "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3"); } - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - platformVersion = await AudioPlayer.platformVersion; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - }); + @override + void dispose() { + _player.dispose(); + super.dispose(); } @override @@ -45,12 +34,121 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( appBar: AppBar( - title: const Text('Plugin example app'), + title: const Text('Audio Player Demo'), ), body: Center( - child: Text('Running on: $_platformVersion\n'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Science Friday"), + Text("Science Friday and WNYC Studios"), + StreamBuilder( + stream: _player.playbackStateStream, + builder: (context, snapshot) { + final state = snapshot.data; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (state == AudioPlaybackState.playing) + IconButton( + icon: Icon(Icons.pause), + iconSize: 64.0, + onPressed: _player.pause, + ) + else if (state == AudioPlaybackState.buffering || + state == AudioPlaybackState.connecting) + Container( + margin: EdgeInsets.all(8.0), + width: 64.0, + height: 64.0, + child: CircularProgressIndicator(), + ) + else + IconButton( + icon: Icon(Icons.play_arrow), + iconSize: 64.0, + onPressed: _player.play, + ), + IconButton( + icon: Icon(Icons.stop), + iconSize: 64.0, + onPressed: state == AudioPlaybackState.stopped || + state == AudioPlaybackState.none + ? null + : _player.stop, + ), + ], + ); + }, + ), + StreamBuilder( + stream: _player.durationStream, + builder: (context, snapshot) { + final duration = snapshot.data ?? Duration.zero; + return StreamBuilder( + stream: _player.getPositionStream(), + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + return SeekBar( + duration: duration, + position: position, + onChangeEnd: (newPosition) { + _player.seek(newPosition); + }, + ); + }, + ); + }, + ), + ], + ), ), ), ); } } + +class SeekBar extends StatefulWidget { + final Duration duration; + final Duration position; + final ValueChanged onChanged; + final ValueChanged onChangeEnd; + + SeekBar({ + @required this.duration, + @required this.position, + this.onChanged, + this.onChangeEnd, + }); + + @override + _SeekBarState createState() => _SeekBarState(); +} + +class _SeekBarState extends State { + double _dragValue; + + @override + Widget build(BuildContext context) { + return Slider( + min: 0.0, + max: widget.duration.inMilliseconds.toDouble(), + value: _dragValue ?? widget.position.inMilliseconds.toDouble(), + onChanged: (value) { + setState(() { + _dragValue = value; + }); + if (widget.onChanged != null) { + widget.onChanged(Duration(milliseconds: value.round())); + } + }, + onChangeEnd: (value) { + _dragValue = null; + if (widget.onChangeEnd != null) { + widget.onChangeEnd(Duration(milliseconds: value.round())); + } + }, + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 023eaec..91377e9 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -74,6 +74,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.4" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.5" pedantic: dependency: transitive description: @@ -81,6 +88,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0+1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" quiver: dependency: transitive description: @@ -88,6 +102,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.22.6" sky_engine: dependency: transitive description: flutter @@ -151,3 +172,4 @@ packages: version: "2.0.8" sdks: dart: ">=2.2.2 <3.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c074f51..39e9d17 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,9 +2,6 @@ name: audio_player_example description: Demonstrates how to use the audio_player plugin. publish_to: 'none' -environment: - sdk: ">=2.1.0 <3.0.0" - dependencies: flutter: sdk: flutter diff --git a/lib/audio_player.dart b/lib/audio_player.dart index fd0a10c..b2ca1d6 100644 --- a/lib/audio_player.dart +++ b/lib/audio_player.dart @@ -1,13 +1,240 @@ 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'); +/// await 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)); +/// await 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, immediately after [stop], and immediately after +/// playback naturally reaches the end of the media. +/// * [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. +/// +/// 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 const MethodChannel _channel = - const MethodChannel('audio_player'); + static final _mainChannel = + MethodChannel('com.ryanheise.audio_player.methods'); - static Future get platformVersion async { - final String version = await _channel.invokeMethod('getPlatformVersion'); - return version; + static Future _createChannel(int id) async { + await _mainChannel.invokeMethod('init', id); + return MethodChannel('com.ryanheise.audio_player.methods.$id'); } + + final Future _channel; + + final int _id; + + Future _durationFuture; + + final _durationSubject = BehaviorSubject(); + + AudioPlayerState _audioPlayerState; + + Stream _eventChannelStream; + + StreamSubscription _eventChannelStreamSubscription; + + final _playerStateSubject = BehaviorSubject(); + + final _playbackStateSubject = BehaviorSubject(); + + /// Creates an [AudioPlayer]. + factory AudioPlayer() => + AudioPlayer._internal(DateTime.now().microsecondsSinceEpoch); + + AudioPlayer._internal(this._id) : _channel = _createChannel(_id) { + _eventChannelStream = EventChannel('com.ryanheise.audio_player.events.$_id') + .receiveBroadcastStream() + .map((data) => _audioPlayerState = AudioPlayerState( + state: AudioPlaybackState.values[data[0]], + updatePosition: Duration(milliseconds: data[1]), + updateTime: Duration(milliseconds: data[2]), + )); + _eventChannelStreamSubscription = + _eventChannelStream.listen(_playerStateSubject.add); + _playbackStateSubject + .addStream(playerStateStream.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 current [AudioPlayerState]. + AudioPlayerState get playerState => _audioPlayerState; + + /// The current [AudioPlayerState]. + Stream get playerStateStream => _playerStateSubject.stream; + + /// The current [AudioPlaybackState]. + Stream get playbackStateStream => + _playbackStateSubject.stream; + + /// A stream periodically tracking the current position of this player. + Stream getPositionStream( + [final Duration period = const Duration(milliseconds: 200)]) => + Observable.combineLatest2( + playerStateStream, + Observable.periodic(period), + (state, _) => state.position); + + /// Loads audio media from a URL and returns the duration of that audio. + 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. + Future setFilePath(final String filePath) => setUrl('file://$filePath'); + + /// Loads audio media from an asset and returns the duration of that audio. + 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, 'audio_player_asset_cache', '$_id')); + + /// Plays the currently loaded media from the current position. It is legal + /// to invoke this method only from one of the following states: + /// + /// * [AudioPlaybackState.stopped] + /// * [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 from any + /// state except for: + /// + /// * [AudioPlaybackState.none] + /// * [AudioPlaybackState.stopped] + Future stop() async { + await _invokeMethod('stop'); + } + + /// Sets the volume of this player, where 1.0 is normal volume. + Future setVolume(final double volume) async { + await _invokeMethod('setVolume', [volume]); + } + + /// Seeks to a particular position. It is legal to invoke this method + /// from any state except for [AudioPlaybackState.none]. + 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. + Future dispose() async { + if ((await _cacheFile).existsSync()) { + (await _cacheFile).deleteSync(); + } + await _invokeMethod('dispose'); + await _durationSubject.close(); + await _eventChannelStreamSubscription.cancel(); + await _playerStateSubject.close(); + } + + Future _invokeMethod(String method, [dynamic args]) async => + (await _channel).invokeMethod(method, args); +} + +/// Encapsulates the playback state and current position of the player. +class AudioPlayerState { + /// 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; + + AudioPlayerState({ + @required this.state, + @required this.updateTime, + @required this.updatePosition, + }); + + /// The current position of the player. + Duration get position => state == AudioPlaybackState.playing + ? updatePosition + + (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch) - + updateTime) + : updatePosition; +} + +/// Enumerates the different playback states of a player. +enum AudioPlaybackState { + none, + stopped, + paused, + playing, + buffering, + connecting, } diff --git a/pubspec.lock b/pubspec.lock index aebdafa..ac76c57 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -54,12 +54,19 @@ packages: source: hosted version: "1.1.7" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted version: "1.6.4" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.5" pedantic: dependency: transitive description: @@ -67,6 +74,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0+1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" quiver: dependency: transitive description: @@ -74,6 +88,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + rxdart: + dependency: "direct main" + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.22.6" sky_engine: dependency: transitive description: flutter @@ -137,3 +158,4 @@ packages: version: "2.0.8" sdks: dart: ">=2.2.2 <3.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index d1e10da..9ae7c73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,16 @@ name: audio_player -description: A new flutter plugin project. +description: Flutter plugin to play audio from streams, files and assets. Works with audio_service to play audio in the background. version: 0.0.1 -author: -homepage: +author: Ryan Heise +homepage: https://pub.dev/packages/audio_service environment: sdk: ">=2.1.0 <3.0.0" dependencies: + rxdart: ^0.22.6 + path: ^1.6.4 + path_provider: ^1.4.5 flutter: sdk: flutter