Dart/Android implementation
This commit is contained in:
parent
64dcdc8e9c
commit
0574c041c5
10 changed files with 688 additions and 51 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue