From a63ef2ba3901515abad765977fce4fc885bf86a3 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Tue, 28 Jul 2020 03:54:00 +1000 Subject: [PATCH] Playlists, looping, shuffling for iOS --- darwin/Classes/AudioPlayer.m | 1239 +++++++++++++++------ darwin/Classes/AudioSource.m | 37 + darwin/Classes/ClippingAudioSource.m | 67 ++ darwin/Classes/ConcatenatingAudioSource.m | 109 ++ darwin/Classes/IndexedAudioSource.m | 64 ++ darwin/Classes/IndexedPlayerItem.m | 16 + darwin/Classes/JustAudioPlugin.m | 1 - darwin/Classes/LoopingAudioSource.m | 53 + darwin/Classes/UriAudioSource.m | 66 ++ example/ios/Podfile.lock | 16 +- example/lib/main.dart | 27 +- example/pubspec.lock | 62 +- ios/Classes/AudioPlayer.h | 6 + ios/Classes/AudioSource.h | 13 + ios/Classes/AudioSource.m | 1 + ios/Classes/ClippingAudioSource.h | 9 + ios/Classes/ClippingAudioSource.m | 1 + ios/Classes/ConcatenatingAudioSource.h | 13 + ios/Classes/ConcatenatingAudioSource.m | 1 + ios/Classes/IndexedAudioSource.h | 20 + ios/Classes/IndexedAudioSource.m | 1 + ios/Classes/IndexedPlayerItem.h | 9 + ios/Classes/IndexedPlayerItem.m | 1 + ios/Classes/LoopingAudioSource.h | 8 + ios/Classes/LoopingAudioSource.m | 1 + ios/Classes/UriAudioSource.h | 8 + ios/Classes/UriAudioSource.m | 1 + lib/just_audio.dart | 44 +- lib/just_audio_web.dart | 2 + macos/Classes/AudioPlayer.h | 8 +- macos/Classes/AudioSource.h | 13 + macos/Classes/AudioSource.m | 1 + macos/Classes/ClippingAudioSource.h | 9 + macos/Classes/ClippingAudioSource.m | 1 + macos/Classes/ConcatenatingAudioSource.h | 13 + macos/Classes/ConcatenatingAudioSource.m | 1 + macos/Classes/IndexedAudioSource.h | 20 + macos/Classes/IndexedAudioSource.m | 1 + macos/Classes/IndexedPlayerItem.h | 9 + macos/Classes/IndexedPlayerItem.m | 1 + macos/Classes/LoopingAudioSource.h | 8 + macos/Classes/LoopingAudioSource.m | 1 + macos/Classes/UriAudioSource.h | 8 + macos/Classes/UriAudioSource.m | 1 + 44 files changed, 1629 insertions(+), 362 deletions(-) create mode 100644 darwin/Classes/AudioSource.m create mode 100644 darwin/Classes/ClippingAudioSource.m create mode 100644 darwin/Classes/ConcatenatingAudioSource.m create mode 100644 darwin/Classes/IndexedAudioSource.m create mode 100644 darwin/Classes/IndexedPlayerItem.m create mode 100644 darwin/Classes/LoopingAudioSource.m create mode 100644 darwin/Classes/UriAudioSource.m create mode 100644 ios/Classes/AudioSource.h create mode 120000 ios/Classes/AudioSource.m create mode 100644 ios/Classes/ClippingAudioSource.h create mode 120000 ios/Classes/ClippingAudioSource.m create mode 100644 ios/Classes/ConcatenatingAudioSource.h create mode 120000 ios/Classes/ConcatenatingAudioSource.m create mode 100644 ios/Classes/IndexedAudioSource.h create mode 120000 ios/Classes/IndexedAudioSource.m create mode 100644 ios/Classes/IndexedPlayerItem.h create mode 120000 ios/Classes/IndexedPlayerItem.m create mode 100644 ios/Classes/LoopingAudioSource.h create mode 120000 ios/Classes/LoopingAudioSource.m create mode 100644 ios/Classes/UriAudioSource.h create mode 120000 ios/Classes/UriAudioSource.m create mode 100644 macos/Classes/AudioSource.h create mode 120000 macos/Classes/AudioSource.m create mode 100644 macos/Classes/ClippingAudioSource.h create mode 120000 macos/Classes/ClippingAudioSource.m create mode 100644 macos/Classes/ConcatenatingAudioSource.h create mode 120000 macos/Classes/ConcatenatingAudioSource.m create mode 100644 macos/Classes/IndexedAudioSource.h create mode 120000 macos/Classes/IndexedAudioSource.m create mode 100644 macos/Classes/IndexedPlayerItem.h create mode 120000 macos/Classes/IndexedPlayerItem.m create mode 100644 macos/Classes/LoopingAudioSource.h create mode 120000 macos/Classes/LoopingAudioSource.m create mode 100644 macos/Classes/UriAudioSource.h create mode 120000 macos/Classes/UriAudioSource.m diff --git a/darwin/Classes/AudioPlayer.m b/darwin/Classes/AudioPlayer.m index 7be8975..dc43d04 100644 --- a/darwin/Classes/AudioPlayer.m +++ b/darwin/Classes/AudioPlayer.m @@ -1,397 +1,994 @@ #import "AudioPlayer.h" +#import "AudioSource.h" +#import "IndexedAudioSource.h" +#import "UriAudioSource.h" +#import "ConcatenatingAudioSource.h" +#import "LoopingAudioSource.h" +#import "ClippingAudioSource.h" #import +#import // TODO: Check for and report invalid state transitions. +// TODO: Apply Apple's guidance on seeking: https://developer.apple.com/library/archive/qa/qa1820/_index.html @implementation AudioPlayer { - NSObject* _registrar; - FlutterMethodChannel* _methodChannel; - FlutterEventChannel* _eventChannel; - FlutterEventSink _eventSink; - NSString* _playerId; - AVPlayer* _player; - enum PlaybackState _state; - long long _updateTime; - int _updatePosition; - CMTime _seekPos; - FlutterResult _connectionResult; - BOOL _buffering; - id _endObserver; - id _timeObserver; - BOOL _automaticallyWaitsToMinimizeStalling; - BOOL _configuredSession; + NSObject* _registrar; + FlutterMethodChannel *_methodChannel; + FlutterEventChannel *_eventChannel; + FlutterEventSink _eventSink; + NSString *_playerId; + AVQueuePlayer *_player; + AudioSource *_audioSource; + NSMutableArray *_indexedAudioSources; + NSMutableArray *_order; + NSMutableArray *_orderInv; + int _index; + enum PlaybackState _state; + enum LoopMode _loopMode; + BOOL _shuffleModeEnabled; + long long _updateTime; + int _updatePosition; + int _lastPosition; + // Set when the current item hasn't been played yet so we aren't sure whether sufficient audio has been buffered. + BOOL _bufferUnconfirmed; + CMTime _seekPos; + FlutterResult _connectionResult; + BOOL _buffering; + id _timeObserver; + BOOL _automaticallyWaitsToMinimizeStalling; + BOOL _configuredSession; + BOOL _playing; } - (instancetype)initWithRegistrar:(NSObject *)registrar playerId:(NSString*)idParam configuredSession:(BOOL)configuredSession { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _registrar = registrar; - _playerId = idParam; - _configuredSession = configuredSession; - _methodChannel = [FlutterMethodChannel - methodChannelWithName:[NSMutableString stringWithFormat:@"com.ryanheise.just_audio.methods.%@", _playerId] - binaryMessenger:[registrar messenger]]; - _eventChannel = [FlutterEventChannel - eventChannelWithName:[NSMutableString stringWithFormat:@"com.ryanheise.just_audio.events.%@", _playerId] - binaryMessenger:[registrar messenger]]; - [_eventChannel setStreamHandler:self]; - _state = none; - _player = nil; - _seekPos = kCMTimeInvalid; - _buffering = NO; - _endObserver = 0; - _timeObserver = 0; - _automaticallyWaitsToMinimizeStalling = YES; - __weak __typeof__(self) weakSelf = self; - [_methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { - [weakSelf handleMethodCall:call result:result]; - }]; - return self; + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _registrar = registrar; + _playerId = idParam; + _configuredSession = configuredSession; + _methodChannel = + [FlutterMethodChannel methodChannelWithName:[NSMutableString stringWithFormat:@"com.ryanheise.just_audio.methods.%@", _playerId] + binaryMessenger:[registrar messenger]]; + _eventChannel = + [FlutterEventChannel eventChannelWithName:[NSMutableString stringWithFormat:@"com.ryanheise.just_audio.events.%@", _playerId] + binaryMessenger:[registrar messenger]]; + [_eventChannel setStreamHandler:self]; + _index = 0; + _state = none; + _loopMode = loopOff; + _shuffleModeEnabled = NO; + _player = nil; + _audioSource = nil; + _indexedAudioSources = nil; + _order = nil; + _orderInv = nil; + _seekPos = kCMTimeInvalid; + _buffering = NO; + _timeObserver = 0; + _updatePosition = 0; + _updateTime = 0; + _lastPosition = 0; + _bufferUnconfirmed = NO; + _playing = NO; + _automaticallyWaitsToMinimizeStalling = YES; + __weak __typeof__(self) weakSelf = self; + [_methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + [weakSelf handleMethodCall:call result:result]; + }]; + return self; } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSArray* args = (NSArray*)call.arguments; - if ([@"setUrl" isEqualToString:call.method]) { - [self setUrl:args[0] result:result]; - } else if ([@"setClip" isEqualToString:call.method]) { - [self setClip:args[0] end:args[1]]; - result(nil); - } else if ([@"play" isEqualToString:call.method]) { - [self play]; - result(nil); - } else if ([@"pause" isEqualToString:call.method]) { - [self pause]; - result(nil); - } else if ([@"stop" isEqualToString:call.method]) { - [self stop]; - result(nil); - } else if ([@"setVolume" isEqualToString:call.method]) { - [self setVolume:(float)[args[0] doubleValue]]; - result(nil); - } else if ([@"setSpeed" isEqualToString:call.method]) { - [self setSpeed:(float)[args[0] doubleValue]]; - result(nil); - } else if ([@"setAutomaticallyWaitsToMinimizeStalling" isEqualToString:call.method]) { - [self setAutomaticallyWaitsToMinimizeStalling:(BOOL)[args[0] boolValue]]; - result(nil); - } else if ([@"seek" isEqualToString:call.method]) { - CMTime position = args[0] == [NSNull null] ? kCMTimePositiveInfinity : CMTimeMake([args[0] intValue], 1000); - [self seek:position result:result]; - result(nil); - } else if ([@"dispose" isEqualToString:call.method]) { - [self dispose]; - result(nil); - } else { - result(FlutterMethodNotImplemented); - } - // TODO - /* } catch (Exception e) { */ - /* e.printStackTrace(); */ - /* result.error("Error", null, null); */ - /* } */ + NSArray* args = (NSArray*)call.arguments; + if ([@"load" isEqualToString:call.method]) { + [self load:args[0] result:result]; + } else if ([@"play" isEqualToString:call.method]) { + [self play]; + result(nil); + } else if ([@"pause" isEqualToString:call.method]) { + [self pause]; + result(nil); + } else if ([@"stop" isEqualToString:call.method]) { + [self stop]; + result(nil); + } else if ([@"setVolume" isEqualToString:call.method]) { + [self setVolume:(float)[args[0] doubleValue]]; + result(nil); + } else if ([@"setSpeed" isEqualToString:call.method]) { + [self setSpeed:(float)[args[0] doubleValue]]; + result(nil); + } else if ([@"setLoopMode" isEqualToString:call.method]) { + [self setLoopMode:[args[0] intValue]]; + result(nil); + } else if ([@"setShuffleModeEnabled" isEqualToString:call.method]) { + [self setShuffleModeEnabled:(BOOL)[args[0] boolValue]]; + result(nil); + } else if ([@"setAutomaticallyWaitsToMinimizeStalling" isEqualToString:call.method]) { + [self setAutomaticallyWaitsToMinimizeStalling:(BOOL)[args[0] boolValue]]; + result(nil); + } else if ([@"seek" isEqualToString:call.method]) { + CMTime position = args[0] == [NSNull null] ? kCMTimePositiveInfinity : CMTimeMake([args[0] intValue], 1000); + [self seek:position index:args[1] completionHandler:^(BOOL finished) { + result(nil); + }]; + result(nil); + } else if ([@"dispose" isEqualToString:call.method]) { + [self dispose]; + result(nil); + } else if ([@"concatenating.add" isEqualToString:call.method]) { + [self concatenatingAdd:(NSString*)args[0] source:(NSDictionary*)args[1]]; + result(nil); + } else if ([@"concatenating.insert" isEqualToString:call.method]) { + [self concatenatingInsert:(NSString*)args[0] index:[args[1] intValue] source:(NSDictionary*)args[2]]; + result(nil); + } else if ([@"concatenating.addAll" isEqualToString:call.method]) { + [self concatenatingAddAll:(NSString*)args[0] sources:(NSArray*)args[1]]; + result(nil); + } else if ([@"concatenating.insertAll" isEqualToString:call.method]) { + [self concatenatingInsertAll:(NSString*)args[0] index:[args[1] intValue] sources:(NSArray*)args[2]]; + result(nil); + } else if ([@"concatenating.removeAt" isEqualToString:call.method]) { + [self concatenatingRemoveAt:(NSString*)args[0] index:(int)args[1]]; + result(nil); + } else if ([@"concatenating.removeRange" isEqualToString:call.method]) { + [self concatenatingRemoveRange:(NSString*)args[0] start:[args[1] intValue] end:[args[2] intValue]]; + result(nil); + } else if ([@"concatenating.move" isEqualToString:call.method]) { + [self concatenatingMove:(NSString*)args[0] currentIndex:[args[1] intValue] newIndex:[args[2] intValue]]; + result(nil); + } else if ([@"concatenating.clear" isEqualToString:call.method]) { + [self concatenatingClear:(NSString*)args[0]]; + result(nil); + } else { + result(FlutterMethodNotImplemented); + } + // TODO + /* } catch (Exception e) { */ + /* e.printStackTrace(); */ + /* result.error("Error", null, null); */ + /* } */ +} + +// Untested +- (void)concatenatingAdd:(NSString *)catId source:(NSDictionary *)source { + [self concatenatingInsertAll:catId index:-1 sources:@[source]]; +} + +// Untested +- (void)concatenatingInsert:(NSString *)catId index:(int)index source:(NSDictionary *)source { + [self concatenatingInsertAll:catId index:index sources:@[source]]; +} + +// Untested +- (void)concatenatingAddAll:(NSString *)catId sources:(NSArray *)sources { + [self concatenatingInsertAll:catId index:-1 sources:sources]; +} + +// Untested +- (void)concatenatingInsertAll:(NSString *)catId index:(int)index sources:(NSArray *)sources { + // Find all duplicates of the identified ConcatenatingAudioSource. + NSMutableArray *matches = [[NSMutableArray alloc] init]; + [_audioSource findById:catId matches:matches]; + // Add each new source to each match. + for (int i = 0; i < matches.count; i++) { + ConcatenatingAudioSource *catSource = (ConcatenatingAudioSource *)matches[i]; + int idx = index >= 0 ? index : catSource.count; + NSMutableArray *audioSources = [self decodeAudioSources:sources]; + for (int j = 0; j < audioSources.count; j++) { + AudioSource *audioSource = audioSources[j]; + [catSource insertSource:audioSource atIndex:(idx + j)]; + } + } + // Index the new audio sources. + _indexedAudioSources = [[NSMutableArray alloc] init]; + [_audioSource buildSequence:_indexedAudioSources treeIndex:0]; + for (int i = 0; i < [_indexedAudioSources count]; i++) { + IndexedAudioSource *audioSource = _indexedAudioSources[i]; + if (!audioSource.isAttached) { + audioSource.playerItem.audioSource = audioSource; + [self addItemObservers:audioSource.playerItem]; + } + } + [self updateOrder]; + if (_player.currentItem) { + _index = [self indexForItem:_player.currentItem]; + } else { + _index = 0; + } + [self enqueueFrom:_index skipMode:NO]; + // Notify each new IndexedAudioSource that it's been attached to the player. + for (int i = 0; i < [_indexedAudioSources count]; i++) { + if (!_indexedAudioSources[i].isAttached) { + [_indexedAudioSources[i] attach:_player]; + } + } + [self broadcastPlaybackEvent]; +} + +// Untested +- (void)concatenatingRemoveAt:(NSString *)catId index:(int)index { + [self concatenatingRemoveRange:catId start:index end:(index + 1)]; +} + +// Untested +- (void)concatenatingRemoveRange:(NSString *)catId start:(int)start end:(int)end { + // Find all duplicates of the identified ConcatenatingAudioSource. + NSMutableArray *matches = [[NSMutableArray alloc] init]; + [_audioSource findById:catId matches:matches]; + // Remove range from each match. + for (int i = 0; i < matches.count; i++) { + ConcatenatingAudioSource *catSource = (ConcatenatingAudioSource *)matches[i]; + int endIndex = end >= 0 ? end : catSource.count; + [catSource removeSourcesFromIndex:start toIndex:endIndex]; + } + // Re-index the remaining audio sources. + NSArray *oldIndexedAudioSources = _indexedAudioSources; + _indexedAudioSources = [[NSMutableArray alloc] init]; + [_audioSource buildSequence:_indexedAudioSources treeIndex:0]; + for (int i = 0, j = 0; i < _indexedAudioSources.count; i++, j++) { + IndexedAudioSource *audioSource = _indexedAudioSources[i]; + while (audioSource != oldIndexedAudioSources[j]) { + [self removeItemObservers:oldIndexedAudioSources[j].playerItem]; + if (j < _index) { + _index--; + } else if (j == _index) { + // The currently playing item was removed. + } + j++; + } + } + [self updateOrder]; + if (_index >= _indexedAudioSources.count) _index = _indexedAudioSources.count - 1; + if (_index < 0) _index = 0; + [self enqueueFrom:_index skipMode:NO]; + [self broadcastPlaybackEvent]; +} + +// Untested +- (void)concatenatingMove:(NSString *)catId currentIndex:(int)currentIndex newIndex:(int)newIndex { + // Find all duplicates of the identified ConcatenatingAudioSource. + NSMutableArray *matches = [[NSMutableArray alloc] init]; + [_audioSource findById:catId matches:matches]; + // Move range within each match. + for (int i = 0; i < matches.count; i++) { + ConcatenatingAudioSource *catSource = (ConcatenatingAudioSource *)matches[i]; + [catSource moveSourceFromIndex:currentIndex toIndex:newIndex]; + } + // Re-index the audio sources. + _indexedAudioSources = [[NSMutableArray alloc] init]; + [_audioSource buildSequence:_indexedAudioSources treeIndex:0]; + _index = [self indexForItem:_player.currentItem]; + [self broadcastPlaybackEvent]; +} + +// Untested +- (void)concatenatingClear:(NSString *)catId { + [self concatenatingRemoveRange:catId start:0 end:-1]; } - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _eventSink = eventSink; - return nil; + _eventSink = eventSink; + return nil; } - (FlutterError*)onCancelWithArguments:(id)arguments { - _eventSink = nil; - return nil; + _eventSink = nil; + return nil; } - (void)checkForDiscontinuity { - if (!_eventSink) return; - if ((_state != playing) && !_buffering) return; - long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0); - int position = [self getCurrentPosition]; - long long timeSinceLastUpdate = now - _updateTime; - long long expectedPosition = _updatePosition + (long long)(timeSinceLastUpdate * _player.rate); - long long drift = position - expectedPosition; - // Update if we've drifted or just started observing - if (_updateTime == 0L) { - [self broadcastPlaybackEvent]; - } else if (drift < -100) { - NSLog(@"time discontinuity detected: %lld", drift); - _buffering = YES; - [self broadcastPlaybackEvent]; - } else if (_buffering) { - _buffering = NO; - [self broadcastPlaybackEvent]; - } + if (!_eventSink) return; + if (!_playing || CMTIME_IS_VALID(_seekPos)) return; + int position = [self getCurrentPosition]; + if (_buffering) { + if (position > _lastPosition) { + NSLog(@"stall ended"); + _buffering = NO; + [self updatePosition]; + [self broadcastPlaybackEvent]; + } + } else { + long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0); + long long timeSinceLastUpdate = now - _updateTime; + long long expectedPosition = _updatePosition + (long long)(timeSinceLastUpdate * _player.rate); + long long drift = position - expectedPosition; + //NSLog(@"position: %d, drift: %lld", position, drift); + // Update if we've drifted or just started observing + if (_updateTime == 0L) { + [self broadcastPlaybackEvent]; + } else if (drift < -100) { + NSLog(@"stall detected: %lld", drift); + _buffering = YES; + [self broadcastPlaybackEvent]; + } + } + _lastPosition = position; } - (void)broadcastPlaybackEvent { - if (!_eventSink) return; - long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0); - _updatePosition = [self getCurrentPosition]; - _updateTime = now; - _eventSink(@[ - @(_state), - @(_buffering), - @(_updatePosition), - @(_updateTime), - // TODO: buffer position - @(_updatePosition), - // TODO: Icy Metadata - [NSNull null], - @([self getDuration]), - ]); + if (!_eventSink) return; + _eventSink(@{ + @"state": @(_state), + @"buffering": @(_buffering), + @"updatePosition": @(_updatePosition), + @"updateTime": @(_updateTime), + // TODO: buffer position + @"bufferedPosition": @(_updatePosition), + // TODO: Icy Metadata + @"icyMetadata": [NSNull null], + @"duration": @([self getDuration]), + @"currentIndex": @(_index), + }); } - (int)getCurrentPosition { - if (_state == none || _state == connecting) { - return 0; - } else if (CMTIME_IS_VALID(_seekPos)) { - return (int)(1000 * CMTimeGetSeconds(_seekPos)); - } else { - return (int)(1000 * CMTimeGetSeconds([_player currentTime])); - } + if (_state == none || _state == connecting) { + return 0; + } else if (CMTIME_IS_VALID(_seekPos)) { + return (int)(1000 * CMTimeGetSeconds(_seekPos)); + } else if (_indexedAudioSources) { + int ms = (int)(1000 * CMTimeGetSeconds(_indexedAudioSources[_index].position)); + if (ms < 0) ms = 0; + return ms; + } else { + return 0; + } } - (int)getDuration { - if (_state == none || _state == connecting) { - return -1; - } else { - return (int)(1000 * CMTimeGetSeconds([[_player currentItem] duration])); - } + if (_state == none) { + return -1; + } else if (_indexedAudioSources) { + int v = (int)(1000 * CMTimeGetSeconds(_indexedAudioSources[_index].duration)); + return v; + } else { + return 0; + } } - (void)setPlaybackState:(enum PlaybackState)state { - //enum PlaybackState oldState = _state; - _state = state; - // TODO: Investigate when we need to start and stop - // observing item position. - /* if (oldState != playing && state == playing) { */ - /* [self startObservingPosition]; */ - /* } */ - [self broadcastPlaybackEvent]; + _state = state; + [self broadcastPlaybackEvent]; } - (void)setPlaybackBufferingState:(enum PlaybackState)state buffering:(BOOL)buffering { - _buffering = buffering; - [self setPlaybackState:state]; + _buffering = buffering; + [self setPlaybackState:state]; } -- (void)setUrl:(NSString*)url result:(FlutterResult)result { - // TODO: error if already connecting - _connectionResult = result; - [self setPlaybackState:connecting]; - if (_player) { - [[_player currentItem] removeObserver:self forKeyPath:@"status"]; - if (@available(macOS 10.12, iOS 10.0, *)) {[_player removeObserver:self forKeyPath:@"timeControlStatus"];} - [[NSNotificationCenter defaultCenter] removeObserver:_endObserver]; - _endObserver = 0; - } - - AVPlayerItem *playerItem; - - //Allow iOs playing both external links and local files. - if ([url hasPrefix:@"file://"]) { - playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[url substringFromIndex:7]]]; - } else { - playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:url]]; - } - - if (@available(macOS 10.13, iOS 11.0, *)) { - // This does the best at reducing distortion on voice - // with speeds below 1.0 - playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain; - } - - [playerItem addObserver:self - forKeyPath:@"status" - options:NSKeyValueObservingOptionNew - context:nil]; - // TODO: Add observer for _endObserver. - _endObserver = [[NSNotificationCenter defaultCenter] - addObserverForName:AVPlayerItemDidPlayToEndTimeNotification - object:playerItem - queue:nil - usingBlock:^(NSNotification* note) { - NSLog(@"Reached play end time"); - [self complete]; - } - ]; - if (_player) { - [_player replaceCurrentItemWithPlayerItem:playerItem]; - } else { - _player = [[AVPlayer alloc] initWithPlayerItem:playerItem]; - } - if (_timeObserver) { - [_player removeTimeObserver:_timeObserver]; - _timeObserver = 0; - } - if (@available(macOS 10.12, iOS 10.0, *)) { - _player.automaticallyWaitsToMinimizeStalling = _automaticallyWaitsToMinimizeStalling; - [_player addObserver:self - forKeyPath:@"timeControlStatus" - options:NSKeyValueObservingOptionNew - context:nil]; - } - // TODO: learn about the different ways to define weakSelf. - //__weak __typeof__(self) weakSelf = self; - //typeof(self) __weak weakSelf = self; - __unsafe_unretained typeof(self) weakSelf = self; - _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(200, 1000) - queue:nil - usingBlock:^(CMTime time) { - [weakSelf checkForDiscontinuity]; - } - ]; - // We send result after the playerItem is ready in observeValueForKeyPath. +- (void)removeItemObservers:(AVPlayerItem *)playerItem { + [playerItem removeObserver:self forKeyPath:@"status"]; + [playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"]; + [playerItem removeObserver:self forKeyPath:@"playbackBufferFull"]; + //[playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:playerItem]; } -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context { +- (void)addItemObservers:(AVPlayerItem *)playerItem { + // Get notified when the item is loaded or had an error loading + [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; + // Get notified of the buffer state + [playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil]; + [playerItem addObserver:self forKeyPath:@"playbackBufferFull" options:NSKeyValueObservingOptionNew context:nil]; + //[playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil]; + // Get notified when playback has reached the end + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onComplete:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; + // Get notified when playback stops due to a failure (currently unused) + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onFailToComplete:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; + // Get notified when playback stalls (currently unused) + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onItemStalled:) name:AVPlayerItemPlaybackStalledNotification object:playerItem]; +} - if ([keyPath isEqualToString:@"status"]) { - AVPlayerItemStatus status = AVPlayerItemStatusUnknown; - NSNumber *statusNumber = change[NSKeyValueChangeNewKey]; - if ([statusNumber isKindOfClass:[NSNumber class]]) { - status = statusNumber.integerValue; - } - switch (status) { - case AVPlayerItemStatusReadyToPlay: - [self setPlaybackState:stopped]; - _connectionResult(@([self getDuration])); - break; - case AVPlayerItemStatusFailed: - NSLog(@"AVPlayerItemStatusFailed"); - _connectionResult([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", _player.currentItem.error] - message:_player.currentItem.error.localizedDescription - details:nil - ]); - break; - case AVPlayerItemStatusUnknown: - break; - } - } - if (@available(macOS 10.12, iOS 10.0, *)) { - if ([keyPath isEqualToString:@"timeControlStatus"]) { - AVPlayerTimeControlStatus status = AVPlayerTimeControlStatusPaused; - NSNumber *statusNumber = change[NSKeyValueChangeNewKey]; - if ([statusNumber isKindOfClass:[NSNumber class]]) { - status = statusNumber.integerValue; +- (NSMutableArray *)decodeAudioSources:(NSArray *)data { + NSMutableArray *array = [[NSMutableArray alloc] init]; + for (int i = 0; i < [data count]; i++) { + AudioSource *source = [self decodeAudioSource:data[i]]; + [array addObject:source]; + } + return array; +} + +- (AudioSource *)decodeAudioSource:(NSDictionary *)data { + NSString *type = data[@"type"]; + if ([@"progressive" isEqualToString:type]) { + return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"]]; + } else if ([@"dash" isEqualToString:type]) { + return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"]]; + } else if ([@"hls" isEqualToString:type]) { + return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"]]; + } else if ([@"concatenating" isEqualToString:type]) { + return [[ConcatenatingAudioSource alloc] initWithId:data[@"id"] + audioSources:[self decodeAudioSources:data[@"audioSources"]]]; + } else if ([@"clipping" isEqualToString:type]) { + return [[ClippingAudioSource alloc] initWithId:data[@"id"] + audioSource:[self decodeAudioSource:data[@"audioSource"]] + start:data[@"start"] + end:data[@"end"]]; + } else if ([@"looping" isEqualToString:type]) { + NSMutableArray *childSources = [NSMutableArray new]; + int count = [data[@"count"] intValue]; + for (int i = 0; i < count; i++) { + [childSources addObject:[self decodeAudioSource:data[@"audioSource"]]]; + } + return [[LoopingAudioSource alloc] initWithId:data[@"id"] audioSources:childSources]; + } else { + return nil; + } +} + +// TODO: remove the skipMode parameter after testing +- (void)enqueueFrom:(int)index skipMode:(BOOL)skipMode { + _index = index; + + // Update the queue while keeping the currently playing item untouched. + + /* NSLog(@"before reorder: _player.items.count: ", _player.items.count); */ + /* [self dumpQueue]; */ + + // First, remove all _player items except for the currently playing one (if any). + IndexedPlayerItem *existingItem = nil; + NSArray *oldPlayerItems = [NSArray arrayWithArray:_player.items]; + for (int i = 0; i < oldPlayerItems.count; i++) { + if (oldPlayerItems[i] != _indexedAudioSources[_index].playerItem) { + [_player removeItem:oldPlayerItems[i]]; + } else { + existingItem = oldPlayerItems[i]; + } + } + + /* NSLog(@"inter order: _player.items.count: ", _player.items.count); */ + /* [self dumpQueue]; */ + + // Regenerate queue + BOOL include = NO; + for (int i = 0; i < [_order count]; i++) { + int si = [_order[i] intValue]; + if (si == _index) include = YES; + if (include && _indexedAudioSources[si].playerItem != existingItem) { + if (!skipMode) { + [_indexedAudioSources[si] seek:kCMTimeZero]; } - switch (status) { - case AVPlayerTimeControlStatusPaused: - [self setPlaybackBufferingState:paused buffering:NO]; - break; - case AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate: - if (_state != stopped) [self setPlaybackBufferingState:stopped buffering:YES]; - else [self setPlaybackBufferingState:connecting buffering:YES]; - break; - case AVPlayerTimeControlStatusPlaying: - [self setPlaybackBufferingState:playing buffering:NO]; - break; + [_player insertItem:_indexedAudioSources[si].playerItem afterItem:nil]; + } + } + + /* NSLog(@"after reorder: _player.items.count: ", _player.items.count); */ + /* [self dumpQueue]; */ + + if (skipMode) { + _buffering = _player.currentItem.playbackBufferEmpty; // || !_player.currentItem.playbackLikelyToKeepUp; + } +} + +- (void)updatePosition { + _updatePosition = [self getCurrentPosition]; + _updateTime = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0); +} + +- (void)load:(NSDictionary *)source result:(FlutterResult)result { + // TODO: error if already connecting + _connectionResult = result; + _index = 0; + [self updatePosition]; + [self setPlaybackState:connecting]; + // Remove previous observers + if (_indexedAudioSources) { + for (int i = 0; i < [_indexedAudioSources count]; i++) { + [self removeItemObservers:_indexedAudioSources[i].playerItem]; + } + } + // Decode audio source + _audioSource = [self decodeAudioSource:source]; + _indexedAudioSources = [[NSMutableArray alloc] init]; + [_audioSource buildSequence:_indexedAudioSources treeIndex:0]; + for (int i = 0; i < [_indexedAudioSources count]; i++) { + IndexedAudioSource *source = _indexedAudioSources[i]; + [self addItemObservers:source.playerItem]; + source.playerItem.audioSource = source; + } + [self updateOrder]; + // Set up an empty player + if (!_player) { + _player = [[AVQueuePlayer alloc] initWithItems:@[]]; + if (@available(macOS 10.12, iOS 10.0, *)) { + _player.automaticallyWaitsToMinimizeStalling = _automaticallyWaitsToMinimizeStalling; + // TODO: Remove these observers in dispose. + [_player addObserver:self + forKeyPath:@"timeControlStatus" + options:NSKeyValueObservingOptionNew + context:nil]; + } + [_player addObserver:self + forKeyPath:@"currentItem" + options:NSKeyValueObservingOptionNew + context:nil]; + // TODO: learn about the different ways to define weakSelf. + //__weak __typeof__(self) weakSelf = self; + //typeof(self) __weak weakSelf = self; + __unsafe_unretained typeof(self) weakSelf = self; + if (!@available(macOS 10.12, iOS 10.0, *)) { + _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(200, 1000) + queue:nil + usingBlock:^(CMTime time) { + [weakSelf checkForDiscontinuity]; + } + ]; + } + } + // Initialise the AVQueuePlayer with items. + [self enqueueFrom:0 skipMode:YES]; + // Notify each IndexedAudioSource that it's been attached to the player. + for (int i = 0; i < [_indexedAudioSources count]; i++) { + [_indexedAudioSources[i] attach:_player]; + } + + // We send result after the playerItem is ready in observeValueForKeyPath. +} + +- (void)updateOrder { + if (_shuffleModeEnabled) { + [_audioSource shuffle:0 currentIndex: _index]; + } + _orderInv = [NSMutableArray arrayWithCapacity:[_indexedAudioSources count]]; + for (int i = 0; i < [_indexedAudioSources count]; i++) { + [_orderInv addObject:@(0)]; + } + if (_shuffleModeEnabled) { + _order = [_audioSource getShuffleOrder]; + } else { + NSMutableArray *order = [[NSMutableArray alloc] init]; + for (int i = 0; i < [_indexedAudioSources count]; i++) { + [order addObject:@(i)]; + } + _order = order; + } + for (int i = 0; i < [_indexedAudioSources count]; i++) { + _orderInv[[_order[i] intValue]] = @(i); + } +} + +- (void)onItemStalled:(NSNotification *)notification { + IndexedPlayerItem *playerItem = (IndexedPlayerItem *)notification.object; + NSLog(@"onItemStalled"); +} + +- (void)onFailToComplete:(NSNotification *)notification { + IndexedPlayerItem *playerItem = (IndexedPlayerItem *)notification.object; + NSLog(@"onFailToComplete"); +} + +- (void)onComplete:(NSNotification *)notification { + if (_loopMode == loopOne) { + [self seek:kCMTimeZero index:@(_index) completionHandler:^(BOOL finished) { + // XXX: Not necessary? + [self play]; + }]; + } else { + IndexedPlayerItem *endedPlayerItem = (IndexedPlayerItem *)notification.object; + IndexedAudioSource *endedSource = endedPlayerItem.audioSource; + // When an item ends, seek back to its beginning. + [endedSource seek:kCMTimeZero]; + + if ([_orderInv[_index] intValue] + 1 < [_order count]) { + // account for automatic move to next item + _index = [_order[[_orderInv[_index] intValue] + 1] intValue]; + NSLog(@"advance to next: index = %d", _index); + [self broadcastPlaybackEvent]; + } else { + // reached end of playlist + if (_loopMode == loopAll) { + NSLog(@"Loop back to first item"); + // Loop back to the beginning + // TODO: Currently there will be a gap at the loop point. + // Maybe we can do something clever by temporarily adding the + // first playlist item at the end of the queue, although this + // will affect any code that assumes the queue always + // corresponds to a contiguous region of the indexed audio + // sources. + // For now we just do a seek back to the start. + if ([_order count] == 1) { + [self seek:kCMTimeZero index:[NSNull null] completionHandler:^(BOOL finished) { + [self play]; + }]; + } else { + [self seek:kCMTimeZero index:_order[0] completionHandler:^(BOOL finished) { + [self play]; + }]; + } + } else { + [self complete]; } } } } -- (void)setClip:(NSNumber*)start end:(NSNumber*)end { - // TODO +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + + if ([keyPath isEqualToString:@"status"]) { + IndexedPlayerItem *playerItem = (IndexedPlayerItem *)object; + AVPlayerItemStatus status = AVPlayerItemStatusUnknown; + NSNumber *statusNumber = change[NSKeyValueChangeNewKey]; + if ([statusNumber isKindOfClass:[NSNumber class]]) { + status = statusNumber.intValue; + } + switch (status) { + case AVPlayerItemStatusReadyToPlay: { + if (playerItem != _player.currentItem) return; + // Detect buffering in different ways depending on whether we're playing + if (_playing) { + if (@available(macOS 10.12, iOS 10.0, *)) { + _buffering = _player.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate; + } else { + // If this happens when we're playing, check whether buffer is confirmed + if (_bufferUnconfirmed && !_player.currentItem.playbackBufferFull) { + // Stay in bufering + } else { + _buffering = _player.currentItem.playbackBufferEmpty;// || !_player.currentItem.playbackLikelyToKeepUp; + } + } + } else { + _buffering = _player.currentItem.playbackBufferEmpty;// || !_player.currentItem.playbackLikelyToKeepUp; + } + // XXX: Maybe if _state == connecting? + // Although then make sure connecting is only used for this purpose. + if (!_playing) { + _state = stopped; + } + [self broadcastPlaybackEvent]; + if (_connectionResult) { + _connectionResult(@([self getDuration])); + _connectionResult = nil; + } + break; + } + case AVPlayerItemStatusFailed: { + NSLog(@"AVPlayerItemStatusFailed"); + if (playerItem != _player.currentItem) return; + if (_connectionResult) { + _connectionResult([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", _player.currentItem.error] + message:_player.currentItem.error.localizedDescription + details:nil]); + _connectionResult = nil; + } + break; + } + case AVPlayerItemStatusUnknown: + break; + } + } else if ([keyPath isEqualToString:@"playbackBufferEmpty"] || [keyPath isEqualToString:@"playbackBufferFull"]) { + // Use these values to detect buffering. + IndexedPlayerItem *playerItem = (IndexedPlayerItem *)object; + if (playerItem != _player.currentItem) return; + // If there's a seek in progress, these values are unreliable + if (CMTIME_IS_VALID(_seekPos)) return; + // Detect buffering in different ways depending on whether we're playing + if (_playing) { + if (@available(macOS 10.12, iOS 10.0, *)) { + // We handle this with timeControlStatus instead. + } else { + if (_bufferUnconfirmed && playerItem.playbackBufferFull) { + _bufferUnconfirmed = NO; + _buffering = NO; + NSLog(@"Buffering confirmed! leaving buffering"); + [self broadcastPlaybackEvent]; + } + } + } else { + if (playerItem.playbackBufferEmpty) { + _buffering = YES; + NSLog(@"[%d] BUFFERING YES: playbackBufferEmpty = %d, playbackBufferFull = %d", [self indexForItem:playerItem], playerItem.playbackBufferEmpty, playerItem.playbackBufferFull); + [self broadcastPlaybackEvent]; + } else if (!playerItem.playbackBufferEmpty || playerItem.playbackBufferFull) { + _buffering = NO; + NSLog(@"[%d] BUFFERING NO: playbackBufferEmpty = %d, playbackBufferFull = %d", [self indexForItem:playerItem], playerItem.playbackBufferEmpty, playerItem.playbackBufferFull); + [self broadcastPlaybackEvent]; + } + } + /* } else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) { */ + } else if ([keyPath isEqualToString:@"timeControlStatus"]) { + if (@available(macOS 10.12, iOS 10.0, *)) { + AVPlayerTimeControlStatus status = AVPlayerTimeControlStatusPaused; + NSNumber *statusNumber = change[NSKeyValueChangeNewKey]; + if ([statusNumber isKindOfClass:[NSNumber class]]) { + status = statusNumber.intValue; + } + switch (status) { + case AVPlayerTimeControlStatusPaused: + //NSLog(@"AVPlayerTimeControlStatusPaused"); + break; + case AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate: + //NSLog(@"AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate"); + _buffering = YES; + [self updatePosition]; + [self broadcastPlaybackEvent]; + break; + case AVPlayerTimeControlStatusPlaying: + //NSLog(@"AVPlayerTimeControlStatusPlaying"); + _buffering = NO; + [self updatePosition]; + [self broadcastPlaybackEvent]; + break; + } + } + } else if ([keyPath isEqualToString:@"currentItem"] && _player.currentItem) { + //NSLog(@"currentItem changed. _index=%d", _index); + _bufferUnconfirmed = YES; + // If we've skipped or transitioned to a new item and we're not + // currently in the middle of a seek + if (_player.currentItem && CMTIME_IS_INVALID(_seekPos) && _player.currentItem.status == AVPlayerItemStatusReadyToPlay) { + [self updatePosition]; + IndexedAudioSource *source = ((IndexedPlayerItem *)_player.currentItem).audioSource; + // We should already be at position zero but for + // ClippingAudioSource it might be off by some milliseconds so we + // consider anything <= 100 as close enough. + if ((int)(1000 * CMTimeGetSeconds(source.position)) > 100) { + NSLog(@"On currentItem change, seeking back to zero"); + BOOL shouldResumePlayback = NO; + AVPlayerActionAtItemEnd originalEndAction = _player.actionAtItemEnd; + if (_playing && CMTimeGetSeconds(CMTimeSubtract(source.position, source.duration)) >= 0) { + NSLog(@"Need to pause while rewinding because we're at the end"); + shouldResumePlayback = YES; + _player.actionAtItemEnd = AVPlayerActionAtItemEndPause; + [_player pause]; + } + _buffering = YES; + [self broadcastPlaybackEvent]; + [source seek:kCMTimeZero completionHandler:^(BOOL finished) { + _buffering = NO; + [self broadcastPlaybackEvent]; + if (shouldResumePlayback) { + _player.actionAtItemEnd = originalEndAction; + // TODO: This logic is almost duplicated in seek. See if we can reuse this code. + [_player play]; + } + }]; + } else { + // Already at zero, no need to seek. + } + } + } +} + +- (int)indexForItem:(IndexedPlayerItem *)playerItem { + for (int i = 0; i < _indexedAudioSources.count; i++) { + if (_indexedAudioSources[i].playerItem == playerItem) { + return i; + } + } + return -1; } - (void)play { - // TODO: dynamically adjust the lag. - //int lag = 6; - //int start = [self getCurrentPosition]; - if (_configuredSession) { - [[AVAudioSession sharedInstance] setActive:YES error:nil]; - } - [_player play]; - if (!@available(macOS 10.12, iOS 10.0, *)) {[self setPlaybackState:playing];} - // TODO: convert this Android code to iOS - /* 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); */ - /* } */ + _playing = YES; + if (_configuredSession) { + [[AVAudioSession sharedInstance] setActive:YES error:nil]; + } + [_player play]; + if (!@available(macOS 10.12, iOS 10.0, *)) { + if (_bufferUnconfirmed && !_player.currentItem.playbackBufferFull) { + _buffering = YES; + } + } + [self updatePosition]; + [self setPlaybackState:playing]; } - (void)pause { - [_player pause]; - if (!@available(macOS 10.12, iOS 10.0, *)) {[self setPlaybackState:paused];} + _playing = NO; + [_player pause]; + [self updatePosition]; + [self setPlaybackState:paused]; } - (void)stop { - [_player pause]; - [_player seekToTime:CMTimeMake(0, 1000) - completionHandler:^(BOOL finished) { - [self setPlaybackBufferingState:stopped buffering:NO]; - }]; + _playing = NO; + [_player pause]; + [self updatePosition]; + [_indexedAudioSources[_index] seek:kCMTimeZero + completionHandler:^(BOOL finished) { + [self updatePosition]; + [self setPlaybackBufferingState:stopped buffering:NO]; + }]; } - (void)complete { - [_player pause]; - [_player seekToTime:CMTimeMake(0, 1000) - completionHandler:^(BOOL finished) { - [self setPlaybackBufferingState:completed buffering:NO]; - }]; + _playing = NO; + [_player pause]; + [self enqueueFrom:_index skipMode:YES]; + [_indexedAudioSources[_index] seek:kCMTimeZero + completionHandler:^(BOOL finished) { + [self setPlaybackBufferingState:completed buffering:NO]; + }]; } - (void)setVolume:(float)volume { - [_player setVolume:volume]; + [_player setVolume:volume]; } - (void)setSpeed:(float)speed { - if (speed == 1.0 - || (speed < 1.0 && _player.currentItem.canPlaySlowForward) - || (speed > 1.0 && _player.currentItem.canPlayFastForward)) { - _player.rate = speed; - } + if (speed == 1.0 + || (speed < 1.0 && _player.currentItem.canPlaySlowForward) + || (speed > 1.0 && _player.currentItem.canPlayFastForward)) { + _player.rate = speed; + } } --(void)setAutomaticallyWaitsToMinimizeStalling:(bool)automaticallyWaitsToMinimizeStalling { - _automaticallyWaitsToMinimizeStalling = automaticallyWaitsToMinimizeStalling; - if (@available(macOS 10.12, iOS 10.0, *)) { - if(_player) { - _player.automaticallyWaitsToMinimizeStalling = automaticallyWaitsToMinimizeStalling; - } - } +- (void)setLoopMode:(int)loopMode { + _loopMode = loopMode; + if (_player) { + switch (_loopMode) { + case loopOne: + _player.actionAtItemEnd = AVPlayerActionAtItemEndPause; // AVPlayerActionAtItemEndNone + break; + default: + _player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance; + } + } } -- (void)seek:(CMTime)position result:(FlutterResult)result { - _seekPos = position; - NSLog(@"seek. enter buffering"); - _buffering = YES; - [self broadcastPlaybackEvent]; - [_player seekToTime:position - completionHandler:^(BOOL finished) { - NSLog(@"seek completed"); - [self onSeekCompletion:result]; - }]; +- (void)setShuffleModeEnabled:(BOOL)shuffleModeEnabled { + NSLog(@"setShuffleModeEnabled: %d", shuffleModeEnabled); + _shuffleModeEnabled = shuffleModeEnabled; + if (!_audioSource) return; + + [self updateOrder]; + + [self enqueueFrom:_index skipMode:NO]; } -- (void)onSeekCompletion:(FlutterResult)result { - _seekPos = kCMTimeInvalid; - _buffering = NO; - [self broadcastPlaybackEvent]; - result(nil); +- (void)dumpQueue { + for (int i = 0; i < _player.items.count; i++) { + IndexedPlayerItem *playerItem = _player.items[i]; + for (int j = 0; j < _indexedAudioSources.count; j++) { + IndexedAudioSource *source = _indexedAudioSources[j]; + if (source.playerItem == playerItem) { + NSLog(@"- %d", j); + break; + } + } + } +} + +- (void)setAutomaticallyWaitsToMinimizeStalling:(bool)automaticallyWaitsToMinimizeStalling { + _automaticallyWaitsToMinimizeStalling = automaticallyWaitsToMinimizeStalling; + if (@available(macOS 10.12, iOS 10.0, *)) { + if(_player) { + _player.automaticallyWaitsToMinimizeStalling = automaticallyWaitsToMinimizeStalling; + } + } +} + +- (void)seek:(CMTime)position index:(NSNumber *)newIndex completionHandler:(void (^)(BOOL))completionHandler { + int index = _index; + if (newIndex != [NSNull null]) { + index = [newIndex intValue]; + } + if (index != _index) { + // Jump to a new item + /* if (_playing && index == _index + 1) { */ + /* // Special case for jumping to the very next item */ + /* NSLog(@"seek to next item: %d -> %d", _index, index); */ + /* [_indexedAudioSources[_index] seek:kCMTimeZero]; */ + /* _index = index; */ + /* [_player advanceToNextItem]; */ + /* [self broadcastPlaybackEvent]; */ + /* } else */ + { + // Jump to a distant item + //NSLog(@"seek# jump to distant item: %d -> %d", _index, index); + if (_playing) { + [_player pause]; + } + [_indexedAudioSources[_index] seek:kCMTimeZero]; + // The "currentItem" key observer will respect that a seek is already in progress + _seekPos = position; + [self updatePosition]; + [self enqueueFrom:index skipMode:YES]; + IndexedAudioSource *source = _indexedAudioSources[_index]; + if (abs((int)(1000 * CMTimeGetSeconds(CMTimeSubtract(source.position, position)))) > 100) { + _buffering = YES; + [self broadcastPlaybackEvent]; + [source seek:position completionHandler:^(BOOL finished) { + if (@available(macOS 10.12, iOS 10.0, *)) { + if (_playing) { + // Handled by timeControlStatus + } else { + if (_bufferUnconfirmed && !_player.currentItem.playbackBufferFull) { + // Stay in buffering + } else if (source.playerItem.status == AVPlayerItemStatusReadyToPlay) { + _buffering = NO; + [self broadcastPlaybackEvent]; + } + } + } else { + if (_bufferUnconfirmed && !_player.currentItem.playbackBufferFull) { + // Stay in buffering + } else if (source.playerItem.status == AVPlayerItemStatusReadyToPlay) { + _buffering = NO; + [self broadcastPlaybackEvent]; + } + } + if (_playing) { + [_player play]; + } + _seekPos = kCMTimeInvalid; + [self broadcastPlaybackEvent]; + if (completionHandler) { + completionHandler(finished); + } + }]; + } else { + _seekPos = kCMTimeInvalid; + if (_playing) { + [_player play]; + } + } + } + } else { + // Seek within an item + if (_playing) { + [_player pause]; + } + _seekPos = position; + //NSLog(@"seek. enter buffering. pos = %d", (int)(1000*CMTimeGetSeconds(_indexedAudioSources[_index].position))); + // TODO: Move this into a separate method so it can also + // be used in skip. + _buffering = YES; + [self broadcastPlaybackEvent]; + [_indexedAudioSources[_index] seek:position completionHandler:^(BOOL finished) { + [self updatePosition]; + if (_playing) { + // If playing, buffering will be detected either by: + // 1. checkForDiscontinuity + // 2. timeControlStatus + [_player play]; + } else { + // If not playing, there is no reliable way to detect + // when buffering has completed, so we use + // !playbackBufferEmpty. Although this always seems to + // be full even right after a seek. + _buffering = _player.currentItem.playbackBufferEmpty; + if (!_buffering) { + [self broadcastPlaybackEvent]; + } + } + _seekPos = kCMTimeInvalid; + [self broadcastPlaybackEvent]; + if (completionHandler) { + completionHandler(finished); + } + }]; + } } - (void)dispose { - if (_state != none) { - [self stop]; - [self setPlaybackBufferingState:none buffering:NO]; - } + if (_state != none) { + [self stop]; + [self setPlaybackBufferingState:none buffering:NO]; + } + if (_timeObserver) { + [_player removeTimeObserver:_timeObserver]; + _timeObserver = 0; + } + if (_indexedAudioSources) { + for (int i = 0; i < [_indexedAudioSources count]; i++) { + [self removeItemObservers:_indexedAudioSources[i].playerItem]; + } + } + if (_player) { + [_player removeObserver:self forKeyPath:@"currentItem"]; + if (@available(macOS 10.12, iOS 10.0, *)) { + [_player removeObserver:self forKeyPath:@"timeControlStatus"]; + } + _player = nil; + } + // Untested: + // [_eventChannel setStreamHandler:nil]; + // [_methodChannel setMethodHandler:nil]; } @end diff --git a/darwin/Classes/AudioSource.m b/darwin/Classes/AudioSource.m new file mode 100644 index 0000000..81534f1 --- /dev/null +++ b/darwin/Classes/AudioSource.m @@ -0,0 +1,37 @@ +#import "AudioSource.h" +#import + +@implementation AudioSource { + NSString *_sourceId; +} + +- (instancetype)initWithId:(NSString *)sid { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _sourceId = sid; + return self; +} + +- (NSString *)sourceId { + return _sourceId; +} + +- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex { + return 0; +} + +- (void)findById:(NSString *)sourceId matches:(NSMutableArray *)matches { + if ([_sourceId isEqualToString:sourceId]) { + [matches addObject:self]; + } +} + +- (NSArray *)getShuffleOrder { + return @[]; +} + +- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex { + return 0; +} + +@end diff --git a/darwin/Classes/ClippingAudioSource.m b/darwin/Classes/ClippingAudioSource.m new file mode 100644 index 0000000..24e006a --- /dev/null +++ b/darwin/Classes/ClippingAudioSource.m @@ -0,0 +1,67 @@ +#import "AudioSource.h" +#import "ClippingAudioSource.h" +#import "IndexedPlayerItem.h" +#import "UriAudioSource.h" +#import + +@implementation ClippingAudioSource { + UriAudioSource *_audioSource; + CMTime _start; + CMTime _end; +} + +- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end { + self = [super initWithId:sid]; + NSAssert(self, @"super init cannot be nil"); + _audioSource = audioSource; + _start = start == [NSNull null] ? kCMTimeZero : CMTimeMake([start intValue], 1000); + _end = end == [NSNull null] ? kCMTimeInvalid : CMTimeMake([end intValue], 1000); + return self; +} + +- (void)findById:(NSString *)sourceId matches:(NSMutableArray *)matches { + [super findById:sourceId matches:matches]; + [_audioSource findById:sourceId matches:matches]; +} + +- (void)attach:(AVQueuePlayer *)player { + [super attach:player]; + _audioSource.playerItem.forwardPlaybackEndTime = _end; + // XXX: Not needed since currentItem observer handles it? + [self seek:kCMTimeZero]; +} + +- (IndexedPlayerItem *)playerItem { + return _audioSource.playerItem; +} + +- (NSArray *)getShuffleOrder { + return @[@(0)]; +} + +- (void)play:(AVQueuePlayer *)player { +} + +- (void)pause:(AVQueuePlayer *)player { +} + +- (void)stop:(AVQueuePlayer *)player { +} + +- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler { + CMTime absPosition = CMTimeAdd(_start, position); + [_audioSource.playerItem seekToTime:absPosition toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler]; +} + +- (CMTime)duration { + return CMTimeSubtract(CMTIME_IS_INVALID(_end) ? self.playerItem.duration : _end, _start); +} + +- (void)setDuration:(CMTime)duration { +} + +- (CMTime)position { + return CMTimeSubtract(self.playerItem.currentTime, _start); +} + +@end diff --git a/darwin/Classes/ConcatenatingAudioSource.m b/darwin/Classes/ConcatenatingAudioSource.m new file mode 100644 index 0000000..bd7b713 --- /dev/null +++ b/darwin/Classes/ConcatenatingAudioSource.m @@ -0,0 +1,109 @@ +#import "AudioSource.h" +#import "ConcatenatingAudioSource.h" +#import +#import + +@implementation ConcatenatingAudioSource { + NSMutableArray *_audioSources; + NSMutableArray *_shuffleOrder; +} + +- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray *)audioSources { + self = [super initWithId:sid]; + NSAssert(self, @"super init cannot be nil"); + _audioSources = audioSources; + return self; +} + +- (int)count { + return _audioSources.count; +} + +- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index { + [_audioSources insertObject:audioSource atIndex:index]; +} + +- (void)removeSourcesFromIndex:(int)start toIndex:(int)end { + if (end == -1) end = _audioSources.count; + for (int i = start; i < end; i++) { + [_audioSources removeObjectAtIndex:start]; + } +} + +- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex { + AudioSource *source = _audioSources[currentIndex]; + [_audioSources removeObjectAtIndex:currentIndex]; + [_audioSources insertObject:source atIndex:newIndex]; +} + +- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex { + for (int i = 0; i < [_audioSources count]; i++) { + treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex]; + } + return treeIndex; +} + +- (void)findById:(NSString *)sourceId matches:(NSMutableArray *)matches { + [super findById:sourceId matches:matches]; + for (int i = 0; i < [_audioSources count]; i++) { + [_audioSources[i] findById:sourceId matches:matches]; + } +} + +- (NSArray *)getShuffleOrder { + NSMutableArray *order = [NSMutableArray new]; + int offset = [order count]; + NSMutableArray *childOrders = [NSMutableArray new]; // array of array of ints + for (int i = 0; i < [_audioSources count]; i++) { + AudioSource *audioSource = _audioSources[i]; + NSArray *childShuffleOrder = [audioSource getShuffleOrder]; + NSMutableArray *offsetChildShuffleOrder = [NSMutableArray new]; + for (int j = 0; j < [childShuffleOrder count]; j++) { + [offsetChildShuffleOrder addObject:@([childShuffleOrder[j] integerValue] + offset)]; + } + [childOrders addObject:offsetChildShuffleOrder]; + offset += [childShuffleOrder count]; + } + for (int i = 0; i < [_audioSources count]; i++) { + [order addObjectsFromArray:childOrders[[_shuffleOrder[i] integerValue]]]; + } + return order; +} + +- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex { + int currentChildIndex = -1; + for (int i = 0; i < [_audioSources count]; i++) { + int indexBefore = treeIndex; + AudioSource *child = _audioSources[i]; + treeIndex = [child shuffle:treeIndex currentIndex:currentIndex]; + if (currentIndex >= indexBefore && currentIndex < treeIndex) { + currentChildIndex = i; + } else {} + } + // Shuffle so that the current child is first in the shuffle order + _shuffleOrder = [NSMutableArray arrayWithCapacity:[_audioSources count]]; + for (int i = 0; i < [_audioSources count]; i++) { + [_shuffleOrder addObject:@(0)]; + } + NSLog(@"shuffle: audioSources.count=%d and shuffleOrder.count=%d", [_audioSources count], [_shuffleOrder count]); + // First generate a random shuffle + for (int i = 0; i < [_audioSources count]; i++) { + int j = arc4random_uniform(i + 1); + _shuffleOrder[i] = _shuffleOrder[j]; + _shuffleOrder[j] = @(i); + } + // Then bring currentIndex to the front + if (currentChildIndex != -1) { + for (int i = 1; i < [_audioSources count]; i++) { + if ([_shuffleOrder[i] integerValue] == currentChildIndex) { + NSNumber *v = _shuffleOrder[0]; + _shuffleOrder[0] = _shuffleOrder[i]; + _shuffleOrder[i] = v; + break; + } + } + } + return treeIndex; +} + +@end diff --git a/darwin/Classes/IndexedAudioSource.m b/darwin/Classes/IndexedAudioSource.m new file mode 100644 index 0000000..c3eecdc --- /dev/null +++ b/darwin/Classes/IndexedAudioSource.m @@ -0,0 +1,64 @@ +#import "IndexedAudioSource.h" +#import "IndexedPlayerItem.h" +#import + +@implementation IndexedAudioSource { + BOOL _isAttached; +} + +- (instancetype)initWithId:(NSString *)sid { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _isAttached = NO; + return self; +} + +- (IndexedPlayerItem *)playerItem { + return nil; +} + +- (BOOL)isAttached { + return _isAttached; +} + +- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex { + [sequence addObject:self]; + return treeIndex + 1; +} + +- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex { + return treeIndex + 1; +} + +- (void)attach:(AVQueuePlayer *)player { + _isAttached = YES; +} + +- (void)play:(AVQueuePlayer *)player { +} + +- (void)pause:(AVQueuePlayer *)player { +} + +- (void)stop:(AVQueuePlayer *)player { +} + +- (void)seek:(CMTime)position { + [self seek:position completionHandler:nil]; +} + +- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler { +} + +- (CMTime)duration { + return kCMTimeInvalid; +} + +- (void)setDuration:(CMTime)duration { +} + +- (CMTime)position { + return kCMTimeInvalid; +} + +@end diff --git a/darwin/Classes/IndexedPlayerItem.m b/darwin/Classes/IndexedPlayerItem.m new file mode 100644 index 0000000..87fafe0 --- /dev/null +++ b/darwin/Classes/IndexedPlayerItem.m @@ -0,0 +1,16 @@ +#import "IndexedPlayerItem.h" +#import "IndexedAudioSource.h" + +@implementation IndexedPlayerItem { + IndexedAudioSource *_audioSource; +} + +-(void)setAudioSource:(IndexedAudioSource *)audioSource { + _audioSource = audioSource; +} + +-(IndexedAudioSource *)audioSource { + return _audioSource; +} + +@end diff --git a/darwin/Classes/JustAudioPlugin.m b/darwin/Classes/JustAudioPlugin.m index 3fedffd..744a243 100644 --- a/darwin/Classes/JustAudioPlugin.m +++ b/darwin/Classes/JustAudioPlugin.m @@ -1,6 +1,5 @@ #import "JustAudioPlugin.h" #import "AudioPlayer.h" -#import "AudioPlayer.h" #import @implementation JustAudioPlugin { diff --git a/darwin/Classes/LoopingAudioSource.m b/darwin/Classes/LoopingAudioSource.m new file mode 100644 index 0000000..aa3cd9d --- /dev/null +++ b/darwin/Classes/LoopingAudioSource.m @@ -0,0 +1,53 @@ +#import "AudioSource.h" +#import "LoopingAudioSource.h" +#import + +@implementation LoopingAudioSource { + // An array of duplicates + NSArray *_audioSources; // +} + +- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray *)audioSources { + self = [super initWithId:sid]; + NSAssert(self, @"super init cannot be nil"); + _audioSources = audioSources; + return self; +} + +- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex { + for (int i = 0; i < [_audioSources count]; i++) { + treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex]; + } + return treeIndex; +} + +- (void)findById:(NSString *)sourceId matches:(NSMutableArray *)matches { + [super findById:sourceId matches:matches]; + for (int i = 0; i < [_audioSources count]; i++) { + [_audioSources[i] findById:sourceId matches:matches]; + } +} + +- (NSArray *)getShuffleOrder { + NSMutableArray *order = [NSMutableArray new]; + int offset = [order count]; + for (int i = 0; i < [_audioSources count]; i++) { + AudioSource *audioSource = _audioSources[i]; + NSArray *childShuffleOrder = [audioSource getShuffleOrder]; + for (int j = 0; j < [childShuffleOrder count]; j++) { + [order addObject:@([childShuffleOrder[j] integerValue] + offset)]; + } + offset += [childShuffleOrder count]; + } + return order; +} + +- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex { + // TODO: This should probably shuffle the same way on all duplicates. + for (int i = 0; i < [_audioSources count]; i++) { + treeIndex = [_audioSources[i] shuffle:treeIndex currentIndex:currentIndex]; + } + return treeIndex; +} + +@end diff --git a/darwin/Classes/UriAudioSource.m b/darwin/Classes/UriAudioSource.m new file mode 100644 index 0000000..0bb07a3 --- /dev/null +++ b/darwin/Classes/UriAudioSource.m @@ -0,0 +1,66 @@ +#import "UriAudioSource.h" +#import "IndexedAudioSource.h" +#import "IndexedPlayerItem.h" +#import + +@implementation UriAudioSource { + NSString *_uri; + IndexedPlayerItem *_playerItem; + /* CMTime _duration; */ +} + +- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri { + self = [super initWithId:sid]; + NSAssert(self, @"super init cannot be nil"); + _uri = uri; + if ([_uri hasPrefix:@"file://"]) { + _playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[_uri substringFromIndex:7]]]; + } else { + _playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL URLWithString:_uri]]; + } + if (@available(macOS 10.13, iOS 11.0, *)) { + // This does the best at reducing distortion on voice with speeds below 1.0 + _playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain; + } + /* NSKeyValueObservingOptions options = */ + /* NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; */ + /* [_playerItem addObserver:self */ + /* forKeyPath:@"duration" */ + /* options:options */ + /* context:nil]; */ + return self; +} + +- (IndexedPlayerItem *)playerItem { + return _playerItem; +} + +- (NSArray *)getShuffleOrder { + return @[@(0)]; +} + +- (void)play:(AVQueuePlayer *)player { +} + +- (void)pause:(AVQueuePlayer *)player { +} + +- (void)stop:(AVQueuePlayer *)player { +} + +- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler { + [_playerItem seekToTime:position toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler]; +} + +- (CMTime)duration { + return _playerItem.duration; +} + +- (void)setDuration:(CMTime)duration { +} + +- (CMTime)position { + return _playerItem.currentTime; +} + +@end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7f7123c..925832c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,11 +4,17 @@ PODS: - Flutter - path_provider (0.0.1): - Flutter + - path_provider_linux (0.0.1): + - Flutter + - path_provider_macos (0.0.1): + - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - just_audio (from `.symlinks/plugins/just_audio/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) + - path_provider_linux (from `.symlinks/plugins/path_provider_linux/ios`) + - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) EXTERNAL SOURCES: Flutter: @@ -17,12 +23,18 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/just_audio/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" + path_provider_linux: + :path: ".symlinks/plugins/path_provider_linux/ios" + path_provider_macos: + :path: ".symlinks/plugins/path_provider_macos/ios" SPEC CHECKSUMS: Flutter: 0e3d915762c693b495b44d77113d4970485de6ec just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa - path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d + path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + path_provider_linux: 4d630dc393e1f20364f3e3b4a2ff41d9674a84e4 + path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 PODFILE CHECKSUM: f32fb4e7c14f8b3ca19a369d7be425dd9241af27 -COCOAPODS: 1.9.1 +COCOAPODS: 1.9.3 diff --git a/example/lib/main.dart b/example/lib/main.dart index b75febe..1526b4d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ -import 'package:flutter/material.dart'; +import 'dart:math'; +import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; import 'package:rxdart/rxdart.dart'; @@ -28,6 +29,27 @@ class _MyAppState extends State { ), ), ), + //LoopingAudioSource( + // count: 2, + // audioSource: AudioSource.uri( + // Uri.parse( + // "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3"), + // tag: AudioMetadata( + // album: "Science Friday", + // title: "A Salute To Head-Scratching Science (full)", + // ), + // ), + //), + //ClippingAudioSource( + // start: Duration(seconds: 60), + // end: Duration(seconds: 65), + // audioSource: AudioSource.uri(Uri.parse( + // "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3")), + // tag: AudioMetadata( + // album: "Science Friday", + // title: "A Salute To Head-Scratching Science (5 seconds)", + // ), + //), AudioSource.uri( Uri.parse( "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3"), @@ -296,7 +318,8 @@ class _SeekBarState extends State { return Slider( min: 0.0, max: widget.duration.inMilliseconds.toDouble(), - value: _dragValue ?? widget.position.inMilliseconds.toDouble(), + value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(), + widget.duration.inMilliseconds.toDouble()), onChanged: (value) { setState(() { _dragValue = value; diff --git a/example/pubspec.lock b/example/pubspec.lock index d35044c..8b2fad3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" async: dependency: transitive description: @@ -22,13 +36,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.3" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" collection: dependency: transitive description: @@ -57,13 +64,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" file: dependency: transitive description: @@ -86,6 +86,13 @@ packages: description: flutter source: sdk version: "0.0.0" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.12" intl: dependency: transitive description: @@ -120,7 +127,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.6.4" path_provider: dependency: transitive description: @@ -149,6 +156,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" platform: dependency: transitive description: @@ -170,6 +184,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.13" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" rxdart: dependency: "direct main" description: @@ -223,7 +244,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.16" + version: "0.2.15" typed_data: dependency: transitive description: @@ -252,6 +273,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.1" sdks: - dart: ">=2.7.0 <3.0.0" + dart: ">=2.6.0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/ios/Classes/AudioPlayer.h b/ios/Classes/AudioPlayer.h index 1938b2c..f0c4494 100644 --- a/ios/Classes/AudioPlayer.h +++ b/ios/Classes/AudioPlayer.h @@ -14,3 +14,9 @@ enum PlaybackState { connecting, completed }; + +enum LoopMode { + loopOff, + loopOne, + loopAll +}; diff --git a/ios/Classes/AudioSource.h b/ios/Classes/AudioSource.h new file mode 100644 index 0000000..c192f33 --- /dev/null +++ b/ios/Classes/AudioSource.h @@ -0,0 +1,13 @@ +#import + +@interface AudioSource : NSObject + +@property (readonly, nonatomic) NSString* sourceId; + +- (instancetype)initWithId:(NSString *)sid; +- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex; +- (void)findById:(NSString *)sourceId matches:(NSMutableArray *)matches; +- (NSArray *)getShuffleOrder; +- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex; + +@end diff --git a/ios/Classes/AudioSource.m b/ios/Classes/AudioSource.m new file mode 120000 index 0000000..16881d6 --- /dev/null +++ b/ios/Classes/AudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/AudioSource.m \ No newline at end of file diff --git a/ios/Classes/ClippingAudioSource.h b/ios/Classes/ClippingAudioSource.h new file mode 100644 index 0000000..6873bdc --- /dev/null +++ b/ios/Classes/ClippingAudioSource.h @@ -0,0 +1,9 @@ +#import "AudioSource.h" +#import "UriAudioSource.h" +#import + +@interface ClippingAudioSource : IndexedAudioSource + +- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end; + +@end diff --git a/ios/Classes/ClippingAudioSource.m b/ios/Classes/ClippingAudioSource.m new file mode 120000 index 0000000..d561b1e --- /dev/null +++ b/ios/Classes/ClippingAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/ClippingAudioSource.m \ No newline at end of file diff --git a/ios/Classes/ConcatenatingAudioSource.h b/ios/Classes/ConcatenatingAudioSource.h new file mode 100644 index 0000000..2c2350a --- /dev/null +++ b/ios/Classes/ConcatenatingAudioSource.h @@ -0,0 +1,13 @@ +#import "AudioSource.h" +#import + +@interface ConcatenatingAudioSource : AudioSource + +@property (readonly, nonatomic) int count; + +- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray *)audioSources; +- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index; +- (void)removeSourcesFromIndex:(int)start toIndex:(int)end; +- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex; + +@end diff --git a/ios/Classes/ConcatenatingAudioSource.m b/ios/Classes/ConcatenatingAudioSource.m new file mode 120000 index 0000000..1e2adbb --- /dev/null +++ b/ios/Classes/ConcatenatingAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/ConcatenatingAudioSource.m \ No newline at end of file diff --git a/ios/Classes/IndexedAudioSource.h b/ios/Classes/IndexedAudioSource.h new file mode 100644 index 0000000..21cb3f1 --- /dev/null +++ b/ios/Classes/IndexedAudioSource.h @@ -0,0 +1,20 @@ +#import "AudioSource.h" +#import "IndexedPlayerItem.h" +#import +#import + +@interface IndexedAudioSource : AudioSource + +@property (readonly, nonatomic) IndexedPlayerItem *playerItem; +@property (readwrite, nonatomic) CMTime duration; +@property (readonly, nonatomic) CMTime position; +@property (readonly, nonatomic) BOOL isAttached; + +- (void)attach:(AVQueuePlayer *)player; +- (void)play:(AVQueuePlayer *)player; +- (void)pause:(AVQueuePlayer *)player; +- (void)stop:(AVQueuePlayer *)player; +- (void)seek:(CMTime)position; +- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler; + +@end diff --git a/ios/Classes/IndexedAudioSource.m b/ios/Classes/IndexedAudioSource.m new file mode 120000 index 0000000..051d504 --- /dev/null +++ b/ios/Classes/IndexedAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/IndexedAudioSource.m \ No newline at end of file diff --git a/ios/Classes/IndexedPlayerItem.h b/ios/Classes/IndexedPlayerItem.h new file mode 100644 index 0000000..5d4a11c --- /dev/null +++ b/ios/Classes/IndexedPlayerItem.h @@ -0,0 +1,9 @@ +#import + +@class IndexedAudioSource; + +@interface IndexedPlayerItem : AVPlayerItem + +@property (readwrite, nonatomic) IndexedAudioSource *audioSource; + +@end diff --git a/ios/Classes/IndexedPlayerItem.m b/ios/Classes/IndexedPlayerItem.m new file mode 120000 index 0000000..04e55fc --- /dev/null +++ b/ios/Classes/IndexedPlayerItem.m @@ -0,0 +1 @@ +../../darwin/Classes/IndexedPlayerItem.m \ No newline at end of file diff --git a/ios/Classes/LoopingAudioSource.h b/ios/Classes/LoopingAudioSource.h new file mode 100644 index 0000000..7c524a9 --- /dev/null +++ b/ios/Classes/LoopingAudioSource.h @@ -0,0 +1,8 @@ +#import "AudioSource.h" +#import + +@interface LoopingAudioSource : AudioSource + +- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray *)audioSources; + +@end diff --git a/ios/Classes/LoopingAudioSource.m b/ios/Classes/LoopingAudioSource.m new file mode 120000 index 0000000..17c7958 --- /dev/null +++ b/ios/Classes/LoopingAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/LoopingAudioSource.m \ No newline at end of file diff --git a/ios/Classes/UriAudioSource.h b/ios/Classes/UriAudioSource.h new file mode 100644 index 0000000..6ee3c2e --- /dev/null +++ b/ios/Classes/UriAudioSource.h @@ -0,0 +1,8 @@ +#import "IndexedAudioSource.h" +#import + +@interface UriAudioSource : IndexedAudioSource + +- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri; + +@end diff --git a/ios/Classes/UriAudioSource.m b/ios/Classes/UriAudioSource.m new file mode 120000 index 0000000..8effbd7 --- /dev/null +++ b/ios/Classes/UriAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/UriAudioSource.m \ No newline at end of file diff --git a/lib/just_audio.dart b/lib/just_audio.dart index d9cd24d..cfe9cdd 100644 --- a/lib/just_audio.dart +++ b/lib/just_audio.dart @@ -136,25 +136,33 @@ class AudioPlayer { _eventChannelStream = EventChannel('com.ryanheise.just_audio.events.$_id') .receiveBroadcastStream() .map((data) { - final duration = (data['duration'] ?? -1) < 0 - ? null - : Duration(milliseconds: data['duration']); - _durationFuture = Future.value(duration); - _durationSubject.add(duration); - _audioPlaybackEvent = AudioPlaybackEvent( - state: AudioPlaybackState.values[data['state']], - buffering: data['buffering'], - updatePosition: Duration(milliseconds: data['updatePosition']), - updateTime: Duration(milliseconds: data['updateTime']), - bufferedPosition: Duration(milliseconds: data['bufferedPosition']), - speed: _speed, - duration: duration, - icyMetadata: data['icyMetadata'] == null + try { + //print("received raw event: $data"); + final duration = (data['duration'] ?? -1) < 0 ? null - : IcyMetadata.fromJson(data['icyMetadata']), - currentIndex: data['currentIndex'], - ); - return _audioPlaybackEvent; + : Duration(milliseconds: data['duration']); + _durationFuture = Future.value(duration); + _durationSubject.add(duration); + _audioPlaybackEvent = AudioPlaybackEvent( + state: AudioPlaybackState.values[data['state']], + buffering: data['buffering'], + updatePosition: Duration(milliseconds: data['updatePosition']), + updateTime: Duration(milliseconds: data['updateTime']), + bufferedPosition: Duration(milliseconds: data['bufferedPosition']), + speed: _speed, + duration: duration, + icyMetadata: data['icyMetadata'] == null + ? null + : IcyMetadata.fromJson(data['icyMetadata']), + currentIndex: data['currentIndex'], + ); + //print("created event object with state: ${_audioPlaybackEvent.state}"); + return _audioPlaybackEvent; + } catch (e, stacktrace) { + print("Error parsing event: $e"); + print("$stacktrace"); + rethrow; + } }); _eventChannelStreamSubscription = _eventChannelStream.listen( _playbackEventSubject.add, diff --git a/lib/just_audio_web.dart b/lib/just_audio_web.dart index d515f5d..55ae75f 100644 --- a/lib/just_audio_web.dart +++ b/lib/just_audio_web.dart @@ -77,6 +77,8 @@ abstract class JustAudioPlayer { return await setLoopMode(args[0]); case 'setShuffleModeEnabled': return await setShuffleModeEnabled(args[0]); + case 'setAutomaticallyWaitsToMinimizeStalling': + return null; case 'seek': return await seek(args[0], args[1]); case 'dispose': diff --git a/macos/Classes/AudioPlayer.h b/macos/Classes/AudioPlayer.h index 78d9cbd..87673c9 100644 --- a/macos/Classes/AudioPlayer.h +++ b/macos/Classes/AudioPlayer.h @@ -2,7 +2,7 @@ @interface AudioPlayer : NSObject -- (instancetype)initWithRegistrar:(NSObject *)registrar playerId:(NSString*)idParam; +- (instancetype)initWithRegistrar:(NSObject *)registrar playerId:(NSString*)idParam configuredSession:(BOOL)configuredSession; @end @@ -14,3 +14,9 @@ enum PlaybackState { connecting, completed }; + +enum LoopMode { + loopOff, + loopOne, + loopAll +}; diff --git a/macos/Classes/AudioSource.h b/macos/Classes/AudioSource.h new file mode 100644 index 0000000..3dd1bf5 --- /dev/null +++ b/macos/Classes/AudioSource.h @@ -0,0 +1,13 @@ +#import + +@interface AudioSource : NSObject + +@property (readonly, nonatomic) NSString* sourceId; + +- (instancetype)initWithId:(NSString *)sid; +- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex; +- (void)findById:(NSString *)sourceId matches:(NSMutableArray *)matches; +- (NSArray *)getShuffleOrder; +- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex; + +@end diff --git a/macos/Classes/AudioSource.m b/macos/Classes/AudioSource.m new file mode 120000 index 0000000..16881d6 --- /dev/null +++ b/macos/Classes/AudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/AudioSource.m \ No newline at end of file diff --git a/macos/Classes/ClippingAudioSource.h b/macos/Classes/ClippingAudioSource.h new file mode 100644 index 0000000..217fcf5 --- /dev/null +++ b/macos/Classes/ClippingAudioSource.h @@ -0,0 +1,9 @@ +#import "AudioSource.h" +#import "UriAudioSource.h" +#import + +@interface ClippingAudioSource : IndexedAudioSource + +- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end; + +@end diff --git a/macos/Classes/ClippingAudioSource.m b/macos/Classes/ClippingAudioSource.m new file mode 120000 index 0000000..d561b1e --- /dev/null +++ b/macos/Classes/ClippingAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/ClippingAudioSource.m \ No newline at end of file diff --git a/macos/Classes/ConcatenatingAudioSource.h b/macos/Classes/ConcatenatingAudioSource.h new file mode 100644 index 0000000..68455af --- /dev/null +++ b/macos/Classes/ConcatenatingAudioSource.h @@ -0,0 +1,13 @@ +#import "AudioSource.h" +#import + +@interface ConcatenatingAudioSource : AudioSource + +@property (readonly, nonatomic) int count; + +- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray *)audioSources; +- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index; +- (void)removeSourcesFromIndex:(int)start toIndex:(int)end; +- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex; + +@end diff --git a/macos/Classes/ConcatenatingAudioSource.m b/macos/Classes/ConcatenatingAudioSource.m new file mode 120000 index 0000000..1e2adbb --- /dev/null +++ b/macos/Classes/ConcatenatingAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/ConcatenatingAudioSource.m \ No newline at end of file diff --git a/macos/Classes/IndexedAudioSource.h b/macos/Classes/IndexedAudioSource.h new file mode 100644 index 0000000..94a6e31 --- /dev/null +++ b/macos/Classes/IndexedAudioSource.h @@ -0,0 +1,20 @@ +#import "AudioSource.h" +#import "IndexedPlayerItem.h" +#import +#import + +@interface IndexedAudioSource : AudioSource + +@property (readonly, nonatomic) IndexedPlayerItem *playerItem; +@property (readwrite, nonatomic) CMTime duration; +@property (readonly, nonatomic) CMTime position; +@property (readonly, nonatomic) BOOL isAttached; + +- (void)attach:(AVQueuePlayer *)player; +- (void)play:(AVQueuePlayer *)player; +- (void)pause:(AVQueuePlayer *)player; +- (void)stop:(AVQueuePlayer *)player; +- (void)seek:(CMTime)position; +- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler; + +@end diff --git a/macos/Classes/IndexedAudioSource.m b/macos/Classes/IndexedAudioSource.m new file mode 120000 index 0000000..051d504 --- /dev/null +++ b/macos/Classes/IndexedAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/IndexedAudioSource.m \ No newline at end of file diff --git a/macos/Classes/IndexedPlayerItem.h b/macos/Classes/IndexedPlayerItem.h new file mode 100644 index 0000000..5d4a11c --- /dev/null +++ b/macos/Classes/IndexedPlayerItem.h @@ -0,0 +1,9 @@ +#import + +@class IndexedAudioSource; + +@interface IndexedPlayerItem : AVPlayerItem + +@property (readwrite, nonatomic) IndexedAudioSource *audioSource; + +@end diff --git a/macos/Classes/IndexedPlayerItem.m b/macos/Classes/IndexedPlayerItem.m new file mode 120000 index 0000000..04e55fc --- /dev/null +++ b/macos/Classes/IndexedPlayerItem.m @@ -0,0 +1 @@ +../../darwin/Classes/IndexedPlayerItem.m \ No newline at end of file diff --git a/macos/Classes/LoopingAudioSource.h b/macos/Classes/LoopingAudioSource.h new file mode 100644 index 0000000..a77636b --- /dev/null +++ b/macos/Classes/LoopingAudioSource.h @@ -0,0 +1,8 @@ +#import "AudioSource.h" +#import + +@interface LoopingAudioSource : AudioSource + +- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray *)audioSources; + +@end diff --git a/macos/Classes/LoopingAudioSource.m b/macos/Classes/LoopingAudioSource.m new file mode 120000 index 0000000..17c7958 --- /dev/null +++ b/macos/Classes/LoopingAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/LoopingAudioSource.m \ No newline at end of file diff --git a/macos/Classes/UriAudioSource.h b/macos/Classes/UriAudioSource.h new file mode 100644 index 0000000..9b74125 --- /dev/null +++ b/macos/Classes/UriAudioSource.h @@ -0,0 +1,8 @@ +#import "IndexedAudioSource.h" +#import + +@interface UriAudioSource : IndexedAudioSource + +- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri; + +@end diff --git a/macos/Classes/UriAudioSource.m b/macos/Classes/UriAudioSource.m new file mode 120000 index 0000000..8effbd7 --- /dev/null +++ b/macos/Classes/UriAudioSource.m @@ -0,0 +1 @@ +../../darwin/Classes/UriAudioSource.m \ No newline at end of file