Dart/Android implementation

This commit is contained in:
Ryan Heise 2019-11-28 16:16:54 +11:00
parent 64dcdc8e9c
commit 0574c041c5
10 changed files with 688 additions and 51 deletions

View File

@ -1,3 +1,3 @@
## 0.0.1 ## 0.0.1
* TODO: Describe initial release. * Initial release with Android implementation first.

View File

@ -1,14 +1,12 @@
# audio_player # 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 * Plays audio from streams, files and assets.
[plug-in package](https://flutter.dev/developing-packages/), * Broadcasts state changes helpful in streaming apps such as `buffering` and `connecting` in addition to the typical `playing`, `paused` and `stopped` states.
a specialized package that includes platform-specific implementation code for * Control audio playback via standard operations: play, pause, stop, setVolume, seek.
Android and/or iOS. * 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 The initial release is for Android. iOS is the next priority.
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -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<Object> event = new ArrayList<Object>();
// 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
}
}

View File

@ -10,16 +10,27 @@ import io.flutter.plugin.common.PluginRegistry.Registrar;
public class AudioPlayerPlugin implements MethodCallHandler { public class AudioPlayerPlugin implements MethodCallHandler {
/** Plugin registration. */ /** Plugin registration. */
public static void registerWith(Registrar registrar) { public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "audio_player"); final MethodChannel channel = new MethodChannel(registrar.messenger(), "com.ryanheise.audio_player.methods");
channel.setMethodCallHandler(new AudioPlayerPlugin()); channel.setMethodCallHandler(new AudioPlayerPlugin(registrar));
} }
private Registrar registrar;
public AudioPlayerPlugin(Registrar registrar) {
this.registrar = registrar;
}
@Override @Override
public void onMethodCall(MethodCall call, Result result) { public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getPlatformVersion")) { switch (call.method) {
result.success("Android " + android.os.Build.VERSION.RELEASE); case "init":
} else { long id = (Long)call.arguments;
result.notImplemented(); new AudioPlayer(registrar, id);
} result.success(null);
break;
default:
result.notImplemented();
break;
}
} }
} }

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:async'; import 'dart:async';
@ -12,32 +14,19 @@ class MyApp extends StatefulWidget {
} }
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown'; final AudioPlayer _player = AudioPlayer();
@override @override
void initState() { void initState() {
super.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. @override
Future<void> initPlatformState() async { void dispose() {
String platformVersion; _player.dispose();
// Platform messages may fail, so we use a try/catch PlatformException. super.dispose();
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 @override
@ -45,12 +34,121 @@ class _MyAppState extends State<MyApp> {
return MaterialApp( return MaterialApp(
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Plugin example app'), title: const Text('Audio Player Demo'),
), ),
body: Center( 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<AudioPlaybackState>(
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<Duration>(
stream: _player.durationStream,
builder: (context, snapshot) {
final duration = snapshot.data ?? Duration.zero;
return StreamBuilder<Duration>(
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<Duration> onChanged;
final ValueChanged<Duration> onChangeEnd;
SeekBar({
@required this.duration,
@required this.position,
this.onChanged,
this.onChangeEnd,
});
@override
_SeekBarState createState() => _SeekBarState();
}
class _SeekBarState extends State<SeekBar> {
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()));
}
},
);
}
}

View File

@ -74,6 +74,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.6.4" version: "1.6.4"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.5"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -81,6 +88,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0+1" version: "1.8.0+1"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
@ -88,6 +102,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.5"
rxdart:
dependency: transitive
description:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.22.6"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -151,3 +172,4 @@ packages:
version: "2.0.8" version: "2.0.8"
sdks: sdks:
dart: ">=2.2.2 <3.0.0" dart: ">=2.2.2 <3.0.0"
flutter: ">=1.9.1+hotfix.5 <2.0.0"

View File

@ -2,9 +2,6 @@ name: audio_player_example
description: Demonstrates how to use the audio_player plugin. description: Demonstrates how to use the audio_player plugin.
publish_to: 'none' publish_to: 'none'
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter

View File

@ -1,13 +1,240 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart'; 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 { class AudioPlayer {
static const MethodChannel _channel = static final _mainChannel =
const MethodChannel('audio_player'); MethodChannel('com.ryanheise.audio_player.methods');
static Future<String> get platformVersion async { static Future<MethodChannel> _createChannel(int id) async {
final String version = await _channel.invokeMethod('getPlatformVersion'); await _mainChannel.invokeMethod('init', id);
return version; return MethodChannel('com.ryanheise.audio_player.methods.$id');
} }
final Future<MethodChannel> _channel;
final int _id;
Future<Duration> _durationFuture;
final _durationSubject = BehaviorSubject<Duration>();
AudioPlayerState _audioPlayerState;
Stream<AudioPlayerState> _eventChannelStream;
StreamSubscription<AudioPlayerState> _eventChannelStreamSubscription;
final _playerStateSubject = BehaviorSubject<AudioPlayerState>();
final _playbackStateSubject = BehaviorSubject<AudioPlaybackState>();
/// 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<Duration> get durationFuture => _durationFuture;
/// The duration of any media set via [setUrl], [setFilePath] or [setAsset].
Stream<Duration> get durationStream => _durationSubject.stream;
/// The current [AudioPlayerState].
AudioPlayerState get playerState => _audioPlayerState;
/// The current [AudioPlayerState].
Stream<AudioPlayerState> get playerStateStream => _playerStateSubject.stream;
/// The current [AudioPlaybackState].
Stream<AudioPlaybackState> get playbackStateStream =>
_playbackStateSubject.stream;
/// A stream periodically tracking the current position of this player.
Stream<Duration> getPositionStream(
[final Duration period = const Duration(milliseconds: 200)]) =>
Observable.combineLatest2<AudioPlayerState, void, Duration>(
playerStateStream,
Observable.periodic(period),
(state, _) => state.position);
/// Loads audio media from a URL and returns the duration of that audio.
Future<Duration> 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<Duration> setFilePath(final String filePath) => setUrl('file://$filePath');
/// Loads audio media from an asset and returns the duration of that audio.
Future<Duration> 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<File> 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<void> 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<void> 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<void> stop() async {
await _invokeMethod('stop');
}
/// Sets the volume of this player, where 1.0 is normal volume.
Future<void> 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<void> 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<void> dispose() async {
if ((await _cacheFile).existsSync()) {
(await _cacheFile).deleteSync();
}
await _invokeMethod('dispose');
await _durationSubject.close();
await _eventChannelStreamSubscription.cancel();
await _playerStateSubject.close();
}
Future<dynamic> _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,
} }

View File

@ -54,12 +54,19 @@ packages:
source: hosted source: hosted
version: "1.1.7" version: "1.1.7"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.6.4" 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: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -67,6 +74,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0+1" version: "1.8.0+1"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
@ -74,6 +88,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.5"
rxdart:
dependency: "direct main"
description:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.22.6"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -137,3 +158,4 @@ packages:
version: "2.0.8" version: "2.0.8"
sdks: sdks:
dart: ">=2.2.2 <3.0.0" dart: ">=2.2.2 <3.0.0"
flutter: ">=1.9.1+hotfix.5 <2.0.0"

View File

@ -1,13 +1,16 @@
name: audio_player 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 version: 0.0.1
author: author: Ryan Heise <ryan@ryanheise.com>
homepage: homepage: https://pub.dev/packages/audio_service
environment: environment:
sdk: ">=2.1.0 <3.0.0" sdk: ">=2.1.0 <3.0.0"
dependencies: dependencies:
rxdart: ^0.22.6
path: ^1.6.4
path_provider: ^1.4.5
flutter: flutter:
sdk: flutter sdk: flutter