Gapless looping on iOS (#273)
This commit is contained in:
parent
a00286229e
commit
ff8e9ef81c
|
@ -40,6 +40,7 @@
|
|||
BOOL _automaticallyWaitsToMinimizeStalling;
|
||||
BOOL _playing;
|
||||
float _speed;
|
||||
BOOL _justAdvanced;
|
||||
}
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar playerId:(NSString*)idParam {
|
||||
|
@ -76,6 +77,7 @@
|
|||
_playResult = nil;
|
||||
_automaticallyWaitsToMinimizeStalling = YES;
|
||||
_speed = 1.0f;
|
||||
_justAdvanced = NO;
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[_methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
|
||||
[weakSelf handleMethodCall:call result:result];
|
||||
|
@ -208,6 +210,9 @@
|
|||
IndexedAudioSource *audioSource = _indexedAudioSources[i];
|
||||
while (audioSource != oldIndexedAudioSources[j]) {
|
||||
[self removeItemObservers:oldIndexedAudioSources[j].playerItem];
|
||||
if (oldIndexedAudioSources[j].playerItem2) {
|
||||
[self removeItemObservers:oldIndexedAudioSources[j].playerItem2];
|
||||
}
|
||||
if (j < _index) {
|
||||
_index--;
|
||||
} else if (j == _index) {
|
||||
|
@ -282,12 +287,12 @@
|
|||
}
|
||||
|
||||
- (void)enterBuffering:(NSString *)reason {
|
||||
NSLog(@"ENTER BUFFERING: %@", reason);
|
||||
//NSLog(@"ENTER BUFFERING: %@", reason);
|
||||
_processingState = buffering;
|
||||
}
|
||||
|
||||
- (void)leaveBuffering:(NSString *)reason {
|
||||
NSLog(@"LEAVE BUFFERING: %@", reason);
|
||||
//NSLog(@"LEAVE BUFFERING: %@", reason);
|
||||
_processingState = ready;
|
||||
}
|
||||
|
||||
|
@ -370,8 +375,8 @@
|
|||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onItemStalled:) name:AVPlayerItemPlaybackStalledNotification object:playerItem];
|
||||
}
|
||||
|
||||
- (NSMutableArray *)decodeAudioSources:(NSArray *)data {
|
||||
NSMutableArray *array = [[NSMutableArray alloc] init];
|
||||
- (NSMutableArray<AudioSource *> *)decodeAudioSources:(NSArray *)data {
|
||||
NSMutableArray<AudioSource *> *array = (NSMutableArray<AudioSource *> *)[[NSMutableArray alloc] init];
|
||||
for (int i = 0; i < [data count]; i++) {
|
||||
AudioSource *source = [self decodeAudioSource:data[i]];
|
||||
[array addObject:source];
|
||||
|
@ -409,6 +414,7 @@
|
|||
}
|
||||
|
||||
- (void)enqueueFrom:(int)index {
|
||||
//NSLog(@"### enqueueFrom:%d", index);
|
||||
_index = index;
|
||||
|
||||
// Update the queue while keeping the currently playing item untouched.
|
||||
|
@ -447,6 +453,7 @@
|
|||
/* [self dumpQueue]; */
|
||||
|
||||
// Regenerate queue
|
||||
if (!existingItem || _loopMode != loopOne) {
|
||||
BOOL include = NO;
|
||||
for (int i = 0; i < [_order count]; i++) {
|
||||
int si = [_order[i] intValue];
|
||||
|
@ -454,8 +461,31 @@
|
|||
if (include && _indexedAudioSources[si].playerItem != existingItem) {
|
||||
//NSLog(@"inserting item %d", si);
|
||||
[_player insertItem:_indexedAudioSources[si].playerItem afterItem:nil];
|
||||
if (_loopMode == loopOne) {
|
||||
// We only want one item in the queue.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add next loop item if we're looping
|
||||
if (_loopMode == loopAll) {
|
||||
int si = [_order[0] intValue];
|
||||
//NSLog(@"### add loop item:%d", si);
|
||||
if (!_indexedAudioSources[si].playerItem2) {
|
||||
[_indexedAudioSources[si] preparePlayerItem2];
|
||||
[self addItemObservers:_indexedAudioSources[si].playerItem2];
|
||||
}
|
||||
[_player insertItem:_indexedAudioSources[si].playerItem2 afterItem:nil];
|
||||
} else if (_loopMode == loopOne) {
|
||||
//NSLog(@"### add loop item:%d", _index);
|
||||
if (!_indexedAudioSources[_index].playerItem2) {
|
||||
[_indexedAudioSources[_index] preparePlayerItem2];
|
||||
[self addItemObservers:_indexedAudioSources[_index].playerItem2];
|
||||
}
|
||||
[_player insertItem:_indexedAudioSources[_index].playerItem2 afterItem:nil];
|
||||
}
|
||||
|
||||
/* NSLog(@"after reorder: _player.items.count: ", _player.items.count); */
|
||||
/* [self dumpQueue]; */
|
||||
|
@ -495,6 +525,9 @@
|
|||
if (_indexedAudioSources) {
|
||||
for (int i = 0; i < [_indexedAudioSources count]; i++) {
|
||||
[self removeItemObservers:_indexedAudioSources[i].playerItem];
|
||||
if (_indexedAudioSources[i].playerItem2) {
|
||||
[self removeItemObservers:_indexedAudioSources[i].playerItem2];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Decode audio source
|
||||
|
@ -572,6 +605,10 @@
|
|||
_player.rate = _speed;
|
||||
}
|
||||
[self broadcastPlaybackEvent];
|
||||
/* NSLog(@"load:"); */
|
||||
/* for (int i = 0; i < [_indexedAudioSources count]; i++) { */
|
||||
/* NSLog(@"- %@", _indexedAudioSources[i].sourceId); */
|
||||
/* } */
|
||||
}
|
||||
|
||||
- (void)updateOrder {
|
||||
|
@ -605,57 +642,29 @@
|
|||
|
||||
- (void)onComplete:(NSNotification *)notification {
|
||||
NSLog(@"onComplete");
|
||||
if (_loopMode == loopOne) {
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[self seek:kCMTimeZero index:@(_index) completionHandler:^(BOOL finished) {
|
||||
// XXX: Not necessary?
|
||||
[weakSelf play];
|
||||
}];
|
||||
} else {
|
||||
|
||||
IndexedPlayerItem *endedPlayerItem = (IndexedPlayerItem *)notification.object;
|
||||
IndexedAudioSource *endedSource = endedPlayerItem.audioSource;
|
||||
|
||||
if ([_orderInv[_index] intValue] + 1 < [_order count]) {
|
||||
// When an item ends, seek back to its beginning.
|
||||
if (_loopMode == loopOne) {
|
||||
[endedSource seek:kCMTimeZero];
|
||||
// account for automatic move to next item
|
||||
_index = [_order[[_orderInv[_index] intValue] + 1] intValue];
|
||||
NSLog(@"advance to next: index = %d", _index);
|
||||
_justAdvanced = YES;
|
||||
} else if (_loopMode == loopAll) {
|
||||
[endedSource seek:kCMTimeZero];
|
||||
_index = [_order[([_orderInv[_index] intValue] + 1) % _order.count] intValue];
|
||||
[self broadcastPlaybackEvent];
|
||||
_justAdvanced = YES;
|
||||
} else if ([_orderInv[_index] intValue] + 1 < [_order count]) {
|
||||
[endedSource seek:kCMTimeZero];
|
||||
_index++;
|
||||
[self updateEndAction];
|
||||
[self broadcastPlaybackEvent];
|
||||
_justAdvanced = YES;
|
||||
} 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) {
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[self seek:kCMTimeZero index:_order[0] completionHandler:^(BOOL finished) {
|
||||
// XXX: Necessary?
|
||||
[weakSelf play];
|
||||
}];
|
||||
} else {
|
||||
// When an item ends, seek back to its beginning.
|
||||
[endedSource seek:kCMTimeZero];
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[self seek:kCMTimeZero index:_order[0] completionHandler:^(BOOL finished) {
|
||||
// XXX: Necessary?
|
||||
[weakSelf play];
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
[self complete];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
|
@ -790,7 +799,9 @@
|
|||
}
|
||||
}
|
||||
} else if ([keyPath isEqualToString:@"currentItem"] && _player.currentItem) {
|
||||
if (_player.currentItem.status == AVPlayerItemStatusFailed) {
|
||||
IndexedPlayerItem *playerItem = (IndexedPlayerItem *)change[NSKeyValueChangeNewKey];
|
||||
IndexedPlayerItem *oldPlayerItem = (IndexedPlayerItem *)change[NSKeyValueChangeOldKey];
|
||||
if (playerItem.status == AVPlayerItemStatusFailed) {
|
||||
if ([_orderInv[_index] intValue] + 1 < [_order count]) {
|
||||
// account for automatic move to next item
|
||||
_index = [_order[[_orderInv[_index] intValue] + 1] intValue];
|
||||
|
@ -802,7 +813,7 @@
|
|||
}
|
||||
return;
|
||||
} else {
|
||||
int expectedIndex = [self indexForItem:(IndexedPlayerItem *)_player.currentItem];
|
||||
int expectedIndex = [self indexForItem:playerItem];
|
||||
if (_index != expectedIndex) {
|
||||
// AVQueuePlayer will sometimes skip over error items without
|
||||
// notifying this observer.
|
||||
|
@ -816,9 +827,9 @@
|
|||
_bufferUnconfirmed = YES;
|
||||
// If we've skipped or transitioned to a new item and we're not
|
||||
// currently in the middle of a seek
|
||||
/* if (CMTIME_IS_INVALID(_seekPos) && _player.currentItem.status == AVPlayerItemStatusReadyToPlay) { */
|
||||
/* if (CMTIME_IS_INVALID(_seekPos) && playerItem.status == AVPlayerItemStatusReadyToPlay) { */
|
||||
/* [self updatePosition]; */
|
||||
/* IndexedAudioSource *source = ((IndexedPlayerItem *)_player.currentItem).audioSource; */
|
||||
/* IndexedAudioSource *source = playerItem.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. */
|
||||
|
@ -850,6 +861,22 @@
|
|||
/* // Already at zero, no need to seek. */
|
||||
/* } */
|
||||
/* } */
|
||||
|
||||
if (_justAdvanced) {
|
||||
IndexedAudioSource *audioSource = playerItem.audioSource;
|
||||
if (_loopMode == loopOne) {
|
||||
[audioSource flip];
|
||||
[self enqueueFrom:_index];
|
||||
} else if (_loopMode == loopAll) {
|
||||
if (_index == [_order[0] intValue] && playerItem == audioSource.playerItem2) {
|
||||
[audioSource flip];
|
||||
[self enqueueFrom:_index];
|
||||
} else {
|
||||
[self updateEndAction];
|
||||
}
|
||||
}
|
||||
_justAdvanced = NO;
|
||||
}
|
||||
} else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
|
||||
IndexedPlayerItem *playerItem = (IndexedPlayerItem *)object;
|
||||
if (playerItem != _player.currentItem) return;
|
||||
|
@ -889,7 +916,7 @@
|
|||
|
||||
- (int)indexForItem:(IndexedPlayerItem *)playerItem {
|
||||
for (int i = 0; i < _indexedAudioSources.count; i++) {
|
||||
if (_indexedAudioSources[i].playerItem == playerItem) {
|
||||
if (_indexedAudioSources[i].playerItem == playerItem || _indexedAudioSources[i].playerItem2 == playerItem) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -989,14 +1016,20 @@
|
|||
}
|
||||
|
||||
- (void)setLoopMode:(int)loopMode {
|
||||
if (loopMode == _loopMode) return;
|
||||
_loopMode = loopMode;
|
||||
[self updateEndAction];
|
||||
[self enqueueFrom:_index];
|
||||
}
|
||||
|
||||
- (void)updateEndAction {
|
||||
// Should update this whenever the audio source changes and whenever _index changes.
|
||||
// Should be called in the following situations:
|
||||
// - when the audio source changes
|
||||
// - when _index changes
|
||||
// - when the loop mode changes.
|
||||
// - when the shuffle order changes. (TODO)
|
||||
// - when the shuffle mode changes.
|
||||
if (!_player) return;
|
||||
if (_audioSource && [_orderInv[_index] intValue] + 1 < [_order count] && _loopMode != loopOne) {
|
||||
if (_audioSource && (_loopMode != loopOff || [_orderInv[_index] intValue] + 1 < [_order count])) {
|
||||
_player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;
|
||||
} else {
|
||||
_player.actionAtItemEnd = AVPlayerActionAtItemEndPause; // AVPlayerActionAtItemEndNone
|
||||
|
@ -1014,19 +1047,15 @@
|
|||
}
|
||||
|
||||
- (void)setShuffleOrder:(NSDictionary *)dict {
|
||||
// TODO: update order and enqueue.
|
||||
[_audioSource decodeShuffleOrder:dict];
|
||||
}
|
||||
|
||||
- (void)dumpQueue {
|
||||
for (int i = 0; i < _player.items.count; i++) {
|
||||
IndexedPlayerItem *playerItem = (IndexedPlayerItem *)_player.items[i];
|
||||
for (int j = 0; j < _indexedAudioSources.count; j++) {
|
||||
IndexedAudioSource *source = _indexedAudioSources[j];
|
||||
if (source.playerItem == playerItem) {
|
||||
int j = [self indexForItem:playerItem];
|
||||
NSLog(@"- %d", j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1189,6 +1218,9 @@
|
|||
if (_indexedAudioSources) {
|
||||
for (int i = 0; i < [_indexedAudioSources count]; i++) {
|
||||
[self removeItemObservers:_indexedAudioSources[i].playerItem];
|
||||
if (_indexedAudioSources[i].playerItem2) {
|
||||
[self removeItemObservers:_indexedAudioSources[i].playerItem2];
|
||||
}
|
||||
}
|
||||
_indexedAudioSources = nil;
|
||||
}
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
|
||||
- (void)attach:(AVQueuePlayer *)player {
|
||||
[super attach:player];
|
||||
// Prepare clip to start/end at the right timestamps.
|
||||
_audioSource.playerItem.forwardPlaybackEndTime = _end;
|
||||
// XXX: Not needed since currentItem observer handles it?
|
||||
[self seek:kCMTimeZero];
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,10 @@
|
|||
return _audioSource.playerItem;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem2 {
|
||||
return _audioSource.playerItem2;
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)getShuffleIndices {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
@ -61,6 +65,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)flip {
|
||||
[_audioSource flip];
|
||||
}
|
||||
|
||||
- (void)preparePlayerItem2 {
|
||||
if (self.playerItem2) return;
|
||||
[_audioSource preparePlayerItem2];
|
||||
IndexedPlayerItem *item = _audioSource.playerItem2;
|
||||
// Prepare loop clip to start/end at the right timestamps.
|
||||
item.forwardPlaybackEndTime = _end;
|
||||
[item seekToTime:_start toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:nil];
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return CMTimeSubtract(CMTIME_IS_INVALID(_end) ? self.playerItem.duration : _end, _start);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid {
|
||||
self = [super init];
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_isAttached = NO;
|
||||
_queuedSeekPos = kCMTimeInvalid;
|
||||
|
@ -33,6 +33,10 @@
|
|||
return nil;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem2 {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isAttached {
|
||||
return _isAttached;
|
||||
}
|
||||
|
@ -66,6 +70,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)flip {
|
||||
}
|
||||
|
||||
- (void)preparePlayerItem2 {
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@implementation UriAudioSource {
|
||||
NSString *_uri;
|
||||
IndexedPlayerItem *_playerItem;
|
||||
IndexedPlayerItem *_playerItem2;
|
||||
/* CMTime _duration; */
|
||||
}
|
||||
|
||||
|
@ -13,28 +14,33 @@
|
|||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_uri = uri;
|
||||
if ([_uri hasPrefix:@"file://"]) {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[[_uri stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] substringFromIndex:7]]];
|
||||
_playerItem = [self createPlayerItem:uri];
|
||||
_playerItem2 = nil;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)createPlayerItem:(NSString *)uri {
|
||||
IndexedPlayerItem *item;
|
||||
if ([uri hasPrefix:@"file://"]) {
|
||||
item = [[IndexedPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[[uri stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] substringFromIndex:7]]];
|
||||
} else {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL URLWithString:_uri]];
|
||||
item = [[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;
|
||||
item.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain;
|
||||
}
|
||||
/* NSKeyValueObservingOptions options = */
|
||||
/* NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; */
|
||||
/* [_playerItem addObserver:self */
|
||||
/* forKeyPath:@"duration" */
|
||||
/* options:options */
|
||||
/* context:nil]; */
|
||||
return self;
|
||||
return item;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return _playerItem;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem2 {
|
||||
return _playerItem2;
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)getShuffleIndices {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
@ -61,6 +67,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)flip {
|
||||
IndexedPlayerItem *temp = _playerItem;
|
||||
_playerItem = _playerItem2;
|
||||
_playerItem2 = temp;
|
||||
}
|
||||
|
||||
- (void)preparePlayerItem2 {
|
||||
if (!_playerItem2) {
|
||||
_playerItem2 = [self createPlayerItem:_uri];
|
||||
_playerItem2.audioSource = _playerItem.audioSource;
|
||||
}
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
NSValue *seekableRange = _playerItem.seekableTimeRanges.lastObject;
|
||||
if (seekableRange) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@interface IndexedAudioSource : AudioSource
|
||||
|
||||
@property (readonly, nonatomic) IndexedPlayerItem *playerItem;
|
||||
@property (readonly, nonatomic) IndexedPlayerItem *playerItem2;
|
||||
@property (readwrite, nonatomic) CMTime duration;
|
||||
@property (readonly, nonatomic) CMTime position;
|
||||
@property (readonly, nonatomic) CMTime bufferedPosition;
|
||||
|
@ -18,5 +19,7 @@
|
|||
- (void)stop:(AVQueuePlayer *)player;
|
||||
- (void)seek:(CMTime)position;
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler;
|
||||
- (void)preparePlayerItem2;
|
||||
- (void)flip;
|
||||
|
||||
@end
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@interface IndexedAudioSource : AudioSource
|
||||
|
||||
@property (readonly, nonatomic) IndexedPlayerItem *playerItem;
|
||||
@property (readonly, nonatomic) IndexedPlayerItem *playerItem2;
|
||||
@property (readwrite, nonatomic) CMTime duration;
|
||||
@property (readonly, nonatomic) CMTime position;
|
||||
@property (readonly, nonatomic) CMTime bufferedPosition;
|
||||
|
@ -18,5 +19,7 @@
|
|||
- (void)stop:(AVQueuePlayer *)player;
|
||||
- (void)seek:(CMTime)position;
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler;
|
||||
- (void)preparePlayerItem2;
|
||||
- (void)flip;
|
||||
|
||||
@end
|
||||
|
|
Loading…
Reference in New Issue