Dart/Android implementation
This commit is contained in:
parent
64dcdc8e9c
commit
0574c041c5
|
@ -1,3 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
||||
* Initial release with Android implementation first.
|
||||
|
|
16
README.md
16
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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MyApp> {
|
||||
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<void> 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<MyApp> {
|
|||
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<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()));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<String> get platformVersion async {
|
||||
final String version = await _channel.invokeMethod('getPlatformVersion');
|
||||
return version;
|
||||
static Future<MethodChannel> _createChannel(int id) async {
|
||||
await _mainChannel.invokeMethod('init', id);
|
||||
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,
|
||||
}
|
||||
|
|
24
pubspec.lock
24
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"
|
||||
|
|
|
@ -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 <ryan@ryanheise.com>
|
||||
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
|
||||
|
||||
|
|
Loading…
Reference in New Issue