diff --git a/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java b/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java index eca1bda..873ae81 100644 --- a/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java +++ b/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java @@ -199,8 +199,8 @@ public class AudioPlayer implements MethodCallHandler { } public void setUrl(final String url, final Result result) throws IOException { - if (state != PlaybackState.none && state != PlaybackState.stopped) { - throw new IllegalStateException("Can call setUrl only from none and stopped states"); + if (state != PlaybackState.none && state != PlaybackState.stopped && state != PlaybackState.completed) { + throw new IllegalStateException("Can call setUrl only from none/stopped/completed states"); } transition(PlaybackState.connecting); this.url = url; @@ -231,6 +231,7 @@ public class AudioPlayer implements MethodCallHandler { this.untilPosition = untilPosition; switch (state) { case stopped: + case completed: ensureStopped(); transition(PlaybackState.playing); playThread = new PlayThread(); @@ -243,7 +244,7 @@ public class AudioPlayer implements MethodCallHandler { } break; default: - throw new IllegalStateException("Can call play only from stopped and paused states (" + state + ")"); + throw new IllegalStateException("Can call play only from stopped, completed and paused states (" + state + ")"); } } @@ -276,6 +277,9 @@ public class AudioPlayer implements MethodCallHandler { switch (state) { case stopped: break; + case completed: + transition(PlaybackState.stopped); + break; // TODO: Allow stopping from buffered state. case playing: case paused: @@ -350,8 +354,8 @@ public class AudioPlayer implements MethodCallHandler { } public void dispose() { - if (state != PlaybackState.stopped && state != PlaybackState.none) { - throw new IllegalStateException("Can call dispose only from stopped and none states"); + if (state != PlaybackState.stopped && state != PlaybackState.completed && state != PlaybackState.none) { + throw new IllegalStateException("Can call dispose only from stopped/completed/none states"); } if (extractor != null) { ensureStopped(); @@ -416,7 +420,7 @@ public class AudioPlayer implements MethodCallHandler { @Override public void run() { - + boolean reachedEnd = false; int encoding = AudioFormat.ENCODING_PCM_16BIT; int channelFormat = channelCount==1?AudioFormat.CHANNEL_OUT_MONO:AudioFormat.CHANNEL_OUT_STEREO; int minSize = AudioTrack.getMinBufferSize(sampleRate, channelFormat, encoding); @@ -532,6 +536,7 @@ public class AudioPlayer implements MethodCallHandler { } else { audioTrack.pause(); finishedDecoding = true; + reachedEnd = true; } } else if (untilPosition != null && currentPosition >= untilPosition) { // NOTE: When streaming audio over bluetooth, it clips off @@ -570,8 +575,9 @@ public class AudioPlayer implements MethodCallHandler { synchronized (monitor) { start = 0; untilPosition = null; - bgTransition(PlaybackState.stopped); + bgTransition(reachedEnd ? PlaybackState.completed : PlaybackState.stopped); extractor.seekTo(0L, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + handler.post(() -> broadcastPlaybackEvent()); playThread = null; monitor.notifyAll(); } @@ -624,7 +630,8 @@ public class AudioPlayer implements MethodCallHandler { paused, playing, buffering, - connecting + connecting, + completed } class SeekRequest { diff --git a/ios/Classes/AudioPlayer.h b/ios/Classes/AudioPlayer.h index cf88631..bee34a8 100644 --- a/ios/Classes/AudioPlayer.h +++ b/ios/Classes/AudioPlayer.h @@ -12,5 +12,6 @@ enum PlaybackState { paused, playing, buffering, - connecting + connecting, + completed }; diff --git a/ios/Classes/AudioPlayer.m b/ios/Classes/AudioPlayer.m index 546068b..c2be099 100644 --- a/ios/Classes/AudioPlayer.m +++ b/ios/Classes/AudioPlayer.m @@ -164,7 +164,7 @@ queue:nil usingBlock:^(NSNotification* note) { NSLog(@"Reached play end time"); - [self stop]; + [self complete]; } ]; if (_player) { @@ -258,6 +258,14 @@ }]; } +- (void)complete { + [_player pause]; + [_player seekToTime:CMTimeMake(0, 1000) + completionHandler:^(BOOL finished) { + [self setPlaybackState:completed]; + }]; +} + - (void)setVolume:(float)volume { [_player setVolume:volume]; } diff --git a/lib/just_audio.dart b/lib/just_audio.dart index bc4f052..e991c78 100644 --- a/lib/just_audio.dart +++ b/lib/just_audio.dart @@ -30,8 +30,7 @@ import 'package:rxdart/rxdart.dart'; /// /// * [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. +/// [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 @@ -40,6 +39,8 @@ import 'package:rxdart/rxdart.dart'; /// 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. @@ -128,7 +129,12 @@ class AudioPlayer { /// The current speed of the player. double get speed => _speed; - /// Loads audio media from a URL and returns the duration of that audio. + /// 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)); @@ -137,11 +143,21 @@ class AudioPlayer { return duration; } - /// Loads audio media from a file and returns the duration of that audio. + /// 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()) { @@ -161,6 +177,7 @@ class AudioPlayer { /// invoke this method only from one of the following states: /// /// * [AudioPlaybackState.stopped] + /// * [AudioPlaybackState.completed] /// * [AudioPlaybackState.paused] Future play({final Duration untilPosition}) async { StreamSubscription subscription; @@ -194,6 +211,7 @@ class AudioPlayer { /// * [AudioPlaybackState.playing] /// * [AudioPlaybackState.paused] /// * [AudioPlaybackState.stopped] + /// * [AudioPlaybackState.completed] Future stop() async { await _invokeMethod('stop'); } @@ -210,14 +228,20 @@ class AudioPlayer { await _invokeMethod('setSpeed', [speed]); } - /// Seeks to a particular position. It is legal to invoke this method - /// from any state except for [AudioPlaybackState.none]. + /// 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. + /// 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(); @@ -275,4 +299,5 @@ enum AudioPlaybackState { playing, buffering, connecting, + completed, }