1// Copyright 2017-present 650 Industries. All rights reserved. 2 3#import <EXAV/EXAVPlayerData.h> 4 5// This struct is passed between the MTAudioProcessingTap callbacks. 6typedef struct AVAudioTapProcessorContext { 7 Boolean supportedTapProcessingFormat; 8 Boolean isNonInterleaved; 9 void *self; // a pointer to EXAVPlayerData 10} AVAudioTapProcessorContext; 11 12NSString *const EXAVPlayerDataStatusIsLoadedKeyPath = @"isLoaded"; 13NSString *const EXAVPlayerDataStatusURIKeyPath = @"uri"; 14NSString *const EXAVPlayerDataStatusHeadersKeyPath = @"headers"; 15NSString *const EXAVPlayerDataStatusProgressUpdateIntervalMillisKeyPath = @"progressUpdateIntervalMillis"; 16NSString *const EXAVPlayerDataStatusDurationMillisKeyPath = @"durationMillis"; 17NSString *const EXAVPlayerDataStatusPositionMillisKeyPath = @"positionMillis"; 18NSString *const EXAVPlayerDataStatusSeekMillisToleranceBeforeKeyPath = @"seekMillisToleranceBefore"; 19NSString *const EXAVPlayerDataStatusSeekMillisToleranceAfterKeyPath = @"seekMillisToleranceAfter"; 20NSString *const EXAVPlayerDataStatusPlayableDurationMillisKeyPath = @"playableDurationMillis"; 21NSString *const EXAVPlayerDataStatusShouldPlayKeyPath = @"shouldPlay"; 22NSString *const EXAVPlayerDataStatusIsPlayingKeyPath = @"isPlaying"; 23NSString *const EXAVPlayerDataStatusIsBufferingKeyPath = @"isBuffering"; 24NSString *const EXAVPlayerDataStatusRateKeyPath = @"rate"; 25NSString *const EXAVPlayerDataStatusPitchCorrectionQualityKeyPath = @"pitchCorrectionQuality"; 26NSString *const EXAVPlayerDataStatusShouldCorrectPitchKeyPath = @"shouldCorrectPitch"; 27NSString *const EXAVPlayerDataStatusVolumeKeyPath = @"volume"; 28NSString *const EXAVPlayerDataStatusIsMutedKeyPath = @"isMuted"; 29NSString *const EXAVPlayerDataStatusIsLoopingKeyPath = @"isLooping"; 30NSString *const EXAVPlayerDataStatusDidJustFinishKeyPath = @"didJustFinish"; 31NSString *const EXAVPlayerDataStatusHasJustBeenInterruptedKeyPath = @"hasJustBeenInterrupted"; 32 33NSString *const EXAVPlayerDataObserverStatusKeyPath = @"status"; 34NSString *const EXAVPlayerDataObserverRateKeyPath = @"rate"; 35NSString *const EXAVPlayerDataObserverCurrentItemKeyPath = @"currentItem"; 36NSString *const EXAVPlayerDataObserverTimeControlStatusPath = @"timeControlStatus"; 37NSString *const EXAVPlayerDataObserverPlaybackBufferEmptyKeyPath = @"playbackBufferEmpty"; 38NSString *const EXAVPlayerDataObserverMetadataKeyPath = @"timedMetadata"; 39 40@interface EXAVPlayerData () 41 42@property (nonatomic, weak) EXAV *exAV; 43 44@property (nonatomic, assign) BOOL isLoaded; 45@property (nonatomic, strong) void (^loadFinishBlock)(BOOL success, NSDictionary *successStatus, NSString *error); 46 47@property (nonatomic, strong) id <NSObject> timeObserver; 48@property (nonatomic, strong) id <NSObject> finishObserver; 49@property (nonatomic, strong) id <NSObject> playbackStalledObserver; 50@property (nonatomic, strong) NSMapTable<NSObject *, NSMutableSet<NSString *> *> *observers; 51 52@property (nonatomic, strong) NSNumber *progressUpdateIntervalMillis; 53@property (nonatomic, assign) CMTime currentPosition; 54@property (nonatomic, assign) BOOL shouldPlay; 55@property (nonatomic, strong) NSNumber *rate; 56@property (nonatomic, strong) NSString *pitchCorrectionQuality; 57@property (nonatomic, strong) NSNumber *observedRate; 58@property (nonatomic, assign) AVPlayerTimeControlStatus timeControlStatus; 59@property (nonatomic, assign) BOOL shouldCorrectPitch; 60@property (nonatomic, strong) NSNumber* volume; 61@property (nonatomic, assign) BOOL isMuted; 62@property (nonatomic, assign) BOOL isLooping; 63@property (nonatomic, strong) NSArray<AVPlayerItem *> *items; 64 65@property (nonatomic, strong) EXPromiseResolveBlock replayResolve; 66 67@end 68 69@implementation EXAVPlayerData 70{ 71 EXAudioSampleCallback* _audioSampleBufferCallback; 72} 73 74#pragma mark - Static methods 75 76+ (NSDictionary *)getUnloadedStatus 77{ 78 return @{EXAVPlayerDataStatusIsLoadedKeyPath: @(NO)}; 79} 80 81#pragma mark - Init and player loading 82 83- (instancetype)initWithEXAV:(EXAV *)exAV 84 withSource:(NSDictionary *)source 85 withStatus:(NSDictionary *)parameters 86 withLoadFinishBlock:(void (^)(BOOL success, NSDictionary *successStatus, NSString *error))loadFinishBlock 87{ 88 if ((self = [super init])) { 89 _exAV = exAV; 90 91 _isLoaded = NO; 92 _loadFinishBlock = loadFinishBlock; 93 94 _player = nil; 95 96 _url = [NSURL URLWithString:[source objectForKey:EXAVPlayerDataStatusURIKeyPath]]; 97 _headers = [self validatedRequestHeaders:source[EXAVPlayerDataStatusHeadersKeyPath]]; 98 99 _timeObserver = nil; 100 _finishObserver = nil; 101 _playbackStalledObserver = nil; 102 _statusUpdateCallback = nil; 103 _observers = [NSMapTable new]; 104 105 // These status props will be potentially reset by the following call to [self setStatus:parameters ...]. 106 _progressUpdateIntervalMillis = @(500); 107 _currentPosition = kCMTimeZero; 108 _timeControlStatus = 0; 109 _shouldPlay = NO; 110 _rate = @(1.0); 111 _pitchCorrectionQuality = AVAudioTimePitchAlgorithmVarispeed; 112 _observedRate = @(1.0); 113 _shouldCorrectPitch = NO; 114 _volume = @(1.0); 115 _isMuted = NO; 116 _isLooping = NO; 117 118 [self setStatus:parameters resolver:nil rejecter:nil]; 119 120 [self _loadNewPlayer]; 121 } 122 123 return self; 124} 125 126- (void)_loadNewPlayer 127{ 128 NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; 129 AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:_url options:@{AVURLAssetHTTPCookiesKey : cookies, @"AVURLAssetHTTPHeaderFieldsKey": _headers}]; 130 131 // unless we preload, the asset will not necessarily load the duration by the time we try to play it. 132 // http://stackoverflow.com/questions/20581567/avplayer-and-avfoundationerrordomain-code-11819 133 EX_WEAKIFY(self); 134 [avAsset loadValuesAsynchronouslyForKeys:@[ @"duration" ] completionHandler:^{ 135 EX_ENSURE_STRONGIFY(self); 136 137 // We prepare three items for AVQueuePlayer, so when the first finishes playing, 138 // second can start playing and the third can start preparing to play. 139 AVPlayerItem *firstplayerItem = [AVPlayerItem playerItemWithAsset:avAsset]; 140 AVPlayerItem *secondPlayerItem = [AVPlayerItem playerItemWithAsset:avAsset]; 141 AVPlayerItem *thirdPlayerItem = [AVPlayerItem playerItemWithAsset:avAsset]; 142 self.items = @[firstplayerItem, secondPlayerItem, thirdPlayerItem]; 143 self.player = [AVQueuePlayer queuePlayerWithItems:@[firstplayerItem, secondPlayerItem, thirdPlayerItem]]; 144 if (self.player) { 145 [self _addObserver:self.player forKeyPath:EXAVPlayerDataObserverStatusKeyPath]; 146 [self _addObserver:self.player.currentItem forKeyPath:EXAVPlayerDataObserverStatusKeyPath]; 147 [self _addObserver:self.player.currentItem forKeyPath:EXAVPlayerDataObserverMetadataKeyPath]; 148 } else { 149 NSString *errorMessage = @"Load encountered an error: [AVPlayer playerWithPlayerItem:] returned nil."; 150 if (self.loadFinishBlock) { 151 self.loadFinishBlock(NO, nil, errorMessage); 152 self.loadFinishBlock = nil; 153 } else if (self.errorCallback) { 154 self.errorCallback(errorMessage); 155 } 156 } 157 }]; 158} 159 160- (void)_finishLoadingNewPlayer 161{ 162 // Set up player with parameters 163 EX_WEAKIFY(self); 164 [_player seekToTime:_currentPosition completionHandler:^(BOOL finished) { 165 EX_ENSURE_STRONGIFY(self); 166 __strong EXAV *strongEXAV = self.exAV; 167 if (strongEXAV) { 168 dispatch_async(self.exAV.methodQueue, ^{ 169 EX_ENSURE_STRONGIFY(self); 170 self.currentPosition = self.player.currentTime; 171 172 self.player.currentItem.audioTimePitchAlgorithm = self.pitchCorrectionQuality; 173 self.player.volume = self.volume.floatValue; 174 self.player.muted = self.isMuted; 175 [self _updateLooping:self.isLooping]; 176 177 [self _tryPlayPlayerWithRateAndMuteIfNecessary]; 178 179 self.isLoaded = YES; 180 181 [self _addObserversForNewPlayer]; 182 183 if (self.loadFinishBlock) { 184 self.loadFinishBlock(YES, [self getStatus], nil); 185 self.loadFinishBlock = nil; 186 } 187 }); 188 } 189 }]; 190} 191 192#pragma mark - setStatus 193 194- (BOOL)_shouldPlayerPlay 195{ 196 return _shouldPlay && ![_rate isEqualToNumber:@(0)]; 197} 198 199- (NSError *)_tryPlayPlayerWithRateAndMuteIfNecessary 200{ 201 if (_player && [self _shouldPlayerPlay]) { 202 NSError *error = [_exAV promoteAudioSessionIfNecessary]; 203 if (!error) { 204 _player.muted = _isMuted; 205 _player.rate = [_rate floatValue]; 206 } 207 return error; 208 } 209 return nil; 210} 211 212- (void)_updateLooping:(BOOL)isLooping 213{ 214 _isLooping = isLooping; 215 if (_isLooping) { 216 [_player setActionAtItemEnd:AVPlayerActionAtItemEndAdvance]; 217 } else { 218 [_player setActionAtItemEnd:AVPlayerActionAtItemEndPause]; 219 } 220} 221 222- (void)setStatus:(NSDictionary *)parameters 223 resolver:(EXPromiseResolveBlock)resolve 224 rejecter:(EXPromiseRejectBlock)reject 225{ 226 BOOL mustUpdateTimeObserver = NO; 227 BOOL mustSeek = NO; 228 229 if ([parameters objectForKey:EXAVPlayerDataStatusProgressUpdateIntervalMillisKeyPath] != nil) { 230 NSNumber *progressUpdateIntervalMillis = parameters[EXAVPlayerDataStatusProgressUpdateIntervalMillisKeyPath]; 231 mustUpdateTimeObserver = ![progressUpdateIntervalMillis isEqualToNumber:_progressUpdateIntervalMillis]; 232 _progressUpdateIntervalMillis = progressUpdateIntervalMillis; 233 } 234 235 // To prevent a race condition, we set _currentPosition at the end of this method. 236 CMTime newPosition = _currentPosition; 237 238 if ([parameters objectForKey:EXAVPlayerDataStatusPositionMillisKeyPath] != nil) { 239 NSNumber *currentPositionMillis = parameters[EXAVPlayerDataStatusPositionMillisKeyPath]; 240 241 // We only seek if the new position is different from _currentPosition by a whole number of milliseconds. 242 mustSeek = currentPositionMillis.longValue != [self _getRoundedMillisFromCMTime:_currentPosition].longValue; 243 if (mustSeek) { 244 newPosition = CMTimeMakeWithSeconds(currentPositionMillis.floatValue / 1000, NSEC_PER_SEC); 245 } 246 } 247 248 // Default values, see: https://developer.apple.com/documentation/avfoundation/avplayer/1388493-seek 249 CMTime toleranceBefore = kCMTimePositiveInfinity; 250 CMTime toleranceAfter = kCMTimePositiveInfinity; 251 252 // We need to set toleranceBefore only if we will seek 253 if (mustSeek && [parameters objectForKey:EXAVPlayerDataStatusSeekMillisToleranceBeforeKeyPath] != nil) { 254 NSNumber *seekMillisToleranceBefore = parameters[EXAVPlayerDataStatusSeekMillisToleranceBeforeKeyPath]; 255 toleranceBefore = CMTimeMakeWithSeconds(seekMillisToleranceBefore.floatValue / 1000, NSEC_PER_SEC); 256 } 257 258 // We need to set toleranceAfter only if we will seek 259 if (mustSeek && [parameters objectForKey:EXAVPlayerDataStatusSeekMillisToleranceAfterKeyPath] != nil) { 260 NSNumber *seekMillisToleranceAfter = parameters[EXAVPlayerDataStatusSeekMillisToleranceAfterKeyPath]; 261 toleranceAfter = CMTimeMakeWithSeconds(seekMillisToleranceAfter.floatValue / 1000, NSEC_PER_SEC); 262 } 263 264 if ([parameters objectForKey:EXAVPlayerDataStatusShouldPlayKeyPath] != nil) { 265 NSNumber *shouldPlay = parameters[EXAVPlayerDataStatusShouldPlayKeyPath]; 266 _shouldPlay = shouldPlay.boolValue; 267 } 268 269 if ([parameters objectForKey:EXAVPlayerDataStatusRateKeyPath] != nil) { 270 NSNumber *rate = parameters[EXAVPlayerDataStatusRateKeyPath]; 271 _rate = rate; 272 } 273 274 if (parameters[EXAVPlayerDataStatusPitchCorrectionQualityKeyPath] != nil) { 275 _pitchCorrectionQuality = parameters[EXAVPlayerDataStatusPitchCorrectionQualityKeyPath]; 276 } 277 278 if ([parameters objectForKey:EXAVPlayerDataStatusShouldCorrectPitchKeyPath] != nil) { 279 NSNumber *shouldCorrectPitch = parameters[EXAVPlayerDataStatusShouldCorrectPitchKeyPath]; 280 _shouldCorrectPitch = shouldCorrectPitch.boolValue; 281 } 282 if ([parameters objectForKey:EXAVPlayerDataStatusVolumeKeyPath] != nil) { 283 NSNumber *volume = parameters[EXAVPlayerDataStatusVolumeKeyPath]; 284 _volume = volume; 285 } 286 if ([parameters objectForKey:EXAVPlayerDataStatusIsMutedKeyPath] != nil) { 287 NSNumber *isMuted = parameters[EXAVPlayerDataStatusIsMutedKeyPath]; 288 _isMuted = isMuted.boolValue; 289 } 290 if ([parameters objectForKey:EXAVPlayerDataStatusIsLoopingKeyPath] != nil) { 291 NSNumber *isLooping = parameters[EXAVPlayerDataStatusIsLoopingKeyPath]; 292 [self _updateLooping:isLooping.boolValue]; 293 } 294 295 if (_player && _isLoaded) { 296 // Pause / mute first if necessary. 297 if (![self _shouldPlayerPlay]) { 298 [_player pause]; 299 } 300 301 if (_isMuted || ![self _isPlayerPlaying]) { 302 _player.muted = _isMuted; 303 } 304 305 // Apply idempotent parameters. 306 if (_shouldCorrectPitch) { 307 _player.currentItem.audioTimePitchAlgorithm = _pitchCorrectionQuality; 308 } else { 309 _player.currentItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmVarispeed; 310 } 311 312 _player.volume = _volume.floatValue; 313 314 // Apply parameters necessary after seek. 315 EX_WEAKIFY(self); 316 void (^applyPostSeekParameters)(BOOL) = ^(BOOL seekSucceeded) { 317 EX_ENSURE_STRONGIFY(self); 318 self.currentPosition = self.player.currentTime; 319 320 if (mustUpdateTimeObserver) { 321 [self _updateTimeObserver]; 322 } 323 324 NSError *audioSessionError = [self _tryPlayPlayerWithRateAndMuteIfNecessary]; 325 326 if (audioSessionError) { 327 if (reject) { 328 reject(@"E_AV_PLAY", @"Play encountered an error: audio session not activated.", audioSessionError); 329 } 330 } else if (!seekSucceeded) { 331 if (reject) { 332 reject(@"E_AV_SEEKING", nil, EXErrorWithMessage(@"Seeking interrupted.")); 333 } 334 } else if (resolve) { 335 resolve([self getStatus]); 336 } 337 338 if (!resolve || !reject) { 339 [self _callStatusUpdateCallback]; 340 } 341 342 [self.exAV demoteAudioSessionIfPossible]; 343 }; 344 345 // Apply seek if necessary. 346 if (mustSeek) { 347 [_player seekToTime:newPosition toleranceBefore:toleranceBefore toleranceAfter:toleranceAfter completionHandler:^(BOOL seekSucceeded) { 348 dispatch_async(self->_exAV.methodQueue, ^{ 349 applyPostSeekParameters(seekSucceeded); 350 }); 351 }]; 352 } else { 353 applyPostSeekParameters(YES); 354 } 355 } else { 356 _currentPosition = newPosition; // Will be set in the new _player on the call to [self _finishLoadingNewPlayer]. 357 if (resolve) { 358 resolve([EXAVPlayerData getUnloadedStatus]); 359 } 360 } 361} 362 363#pragma mark - getStatus 364 365- (BOOL)_isPlayerPlaying 366{ 367 if ([_player respondsToSelector:@selector(timeControlStatus)]) { 368 // Only available after iOS 10 369 return [_player timeControlStatus] == AVPlayerTimeControlStatusPlaying; 370 } else { 371 // timeControlStatus is preferable to this when available 372 // See http://stackoverflow.com/questions/5655864/check-play-state-of-avplayer 373 return _player.rate != 0 && _player.error == nil; 374 } 375} 376 377- (NSNumber *)_getRoundedMillisFromCMTime:(CMTime)time 378{ 379 return CMTIME_IS_INVALID(time) || CMTIME_IS_INDEFINITE(time) ? nil : @((long) (CMTimeGetSeconds(time) * 1000)); 380} 381 382- (NSNumber *)_getClippedValueForValue:(NSNumber *)value withMin:(NSNumber *)min withMax:(NSNumber *)max 383{ 384 return (min != nil && [value doubleValue] < [min doubleValue]) ? min 385 : (max != nil && [value doubleValue] > [max doubleValue]) ? max 386 : value; 387} 388 389- (double)getCurrentPositionPrecise 390{ 391 NSNumber *durationMillis = [self _getRoundedMillisFromCMTime:_player.currentItem.duration]; 392 if (durationMillis) { 393 durationMillis = @(MAX(durationMillis.longValue, 0)); 394 } 395 396 NSNumber *positionMillis = [self _getRoundedMillisFromCMTime:[_player currentTime]]; 397 positionMillis = [self _getClippedValueForValue:positionMillis withMin:@(0) withMax:durationMillis]; 398 return positionMillis.doubleValue / 1000.0; 399} 400 401- (NSDictionary *)getStatus 402{ 403 if (!_isLoaded || _player == nil) { 404 return [EXAVPlayerData getUnloadedStatus]; 405 } 406 407 AVPlayerItem *currentItem = _player.currentItem; 408 if (_player.status != AVPlayerStatusReadyToPlay || currentItem.status != AVPlayerItemStatusReadyToPlay) { 409 return [EXAVPlayerData getUnloadedStatus]; 410 } 411 412 // Get duration and position: 413 NSNumber *durationMillis = [self _getRoundedMillisFromCMTime:currentItem.duration]; 414 if (durationMillis) { 415 durationMillis = [durationMillis doubleValue] < 0 ? 0 : durationMillis; 416 } 417 418 NSNumber *positionMillis = [self _getRoundedMillisFromCMTime:[_player currentTime]]; 419 positionMillis = [self _getClippedValueForValue:positionMillis withMin:@(0) withMax:durationMillis]; 420 421 // Calculate playable duration: 422 NSNumber *playableDurationMillis; 423 if (_player.status == AVPlayerStatusReadyToPlay) { 424 __block CMTimeRange effectiveTimeRange; 425 [currentItem.loadedTimeRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 426 CMTimeRange timeRange = [obj CMTimeRangeValue]; 427 if (CMTimeRangeContainsTime(timeRange, currentItem.currentTime)) { 428 effectiveTimeRange = timeRange; 429 *stop = YES; 430 } 431 }]; 432 playableDurationMillis = [self _getRoundedMillisFromCMTime:CMTimeRangeGetEnd(effectiveTimeRange)]; 433 if (playableDurationMillis) { 434 playableDurationMillis = [self _getClippedValueForValue:playableDurationMillis withMin:@(0) withMax:durationMillis]; 435 } 436 } 437 438 // Calculate if the player is buffering 439 BOOL isPlaying = [self _isPlayerPlaying]; 440 BOOL isBuffering; 441 if (isPlaying) { 442 isBuffering = NO; 443 } else if ([_player respondsToSelector:@selector(timeControlStatus)]) { 444 // Only available after iOS 10 445 isBuffering = _player.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate; 446 } else { 447 isBuffering = !_player.currentItem.isPlaybackLikelyToKeepUp && _player.currentItem.isPlaybackBufferEmpty; 448 } 449 450 // TODO : react-native-video includes the iOS-only keys seekableDuration and canReverse (etc) flags. 451 // Consider adding these. 452 NSMutableDictionary *mutableStatus = [@{EXAVPlayerDataStatusIsLoadedKeyPath: @(YES), 453 454 EXAVPlayerDataStatusURIKeyPath: [_url absoluteString], 455 456 EXAVPlayerDataStatusProgressUpdateIntervalMillisKeyPath: _progressUpdateIntervalMillis, 457 // positionMillis, playableDurationMillis, and durationMillis may be nil and are added after this definition. 458 459 EXAVPlayerDataStatusShouldPlayKeyPath: @(_shouldPlay), 460 EXAVPlayerDataStatusIsPlayingKeyPath: @(isPlaying), 461 EXAVPlayerDataStatusIsBufferingKeyPath: @(isBuffering), 462 463 EXAVPlayerDataStatusRateKeyPath: _rate, 464 EXAVPlayerDataStatusShouldCorrectPitchKeyPath: @(_shouldCorrectPitch), 465 EXAVPlayerDataStatusPitchCorrectionQualityKeyPath: _pitchCorrectionQuality, 466 EXAVPlayerDataStatusVolumeKeyPath: @(_player.volume), 467 EXAVPlayerDataStatusIsMutedKeyPath: @(_player.muted), 468 EXAVPlayerDataStatusIsLoopingKeyPath: @(_isLooping), 469 470 EXAVPlayerDataStatusDidJustFinishKeyPath: @(NO), 471 EXAVPlayerDataStatusHasJustBeenInterruptedKeyPath: @(NO), 472 } mutableCopy]; 473 474 mutableStatus[EXAVPlayerDataStatusPlayableDurationMillisKeyPath] = playableDurationMillis; 475 mutableStatus[EXAVPlayerDataStatusDurationMillisKeyPath] = durationMillis; 476 mutableStatus[EXAVPlayerDataStatusPositionMillisKeyPath] = positionMillis; 477 478 return mutableStatus; 479} 480 481- (void)_callStatusUpdateCallbackWithExtraFields:(NSDictionary *)extraFields 482{ 483 NSDictionary *status; 484 if (extraFields) { 485 NSMutableDictionary *mutableStatus = [[self getStatus] mutableCopy]; 486 [mutableStatus addEntriesFromDictionary:extraFields]; 487 status = mutableStatus; 488 } else { 489 status = [self getStatus]; 490 } 491 if (_statusUpdateCallback) { 492 _statusUpdateCallback(status); 493 } 494} 495 496- (void)_callStatusUpdateCallback 497{ 498 [self _callStatusUpdateCallbackWithExtraFields:nil]; 499} 500 501#pragma mark - Replay 502 503- (void)replayWithStatus:(NSDictionary *)status 504 resolver:(EXPromiseResolveBlock)resolve 505 rejecter:(EXPromiseRejectBlock)reject 506{ 507 [self _callStatusUpdateCallbackWithExtraFields:@{ 508 EXAVPlayerDataStatusHasJustBeenInterruptedKeyPath: @([self _isPlayerPlaying]), 509 }]; 510 // Player is in a prepared state and not playing, so we can just start to play with a regular `setStatus`. 511 if (![self _isPlayerPlaying] && CMTimeCompare(_player.currentTime, kCMTimeZero) == 0) { 512 [self setStatus:status resolver:resolve rejecter:reject]; 513 } else if ([_player.items count] > 1) { 514 // There is an item ahead of the current item in the queue, so we can just advance to it (it should be seeked to 0) 515 // and start to play with `setStatus`. 516 [_player advanceToNextItem]; 517 [self setStatus:status resolver:resolve rejecter:reject]; 518 } else { 519 // There is no item that we could advance to (replays happened to fast), so let's wait for the seeks to finish. 520 // Then they will be added to the queue and the player will start to play, which we will know with KVO on `rate` or `timeControlStatus`. 521 _replayResolve = resolve; 522 if (status != nil) { 523 [self setStatus:status resolver:nil rejecter:nil]; 524 } 525 } 526} 527 528#pragma mark - Observers 529 530- (void)_addObserver:(NSObject *)object forKeyPath:(NSString *)path 531{ 532 [self _addObserver:object forKeyPath:path options:0]; 533} 534 535- (void)_addObserver:(NSObject *)object forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options 536{ 537 @synchronized(_observers) { 538 NSMutableSet<NSString *> *set = [_observers objectForKey:object]; 539 if (set == nil) { 540 set = [NSMutableSet set]; 541 [_observers setObject:set forKey:object]; 542 } 543 if (![set containsObject:path]) { 544 [set addObject:path]; 545 [object addObserver:self forKeyPath:path options:options context:nil]; 546 } 547 } 548} 549 550- (void)_tryRemoveObserver:(NSObject *)object forKeyPath:(NSString *)path 551{ 552 @synchronized(_observers) { 553 NSMutableSet<NSString *> *set = [_observers objectForKey:object]; 554 if (set) { 555 if ([set containsObject:path]) { 556 [set removeObject:path]; 557 if (!set.count) { 558 [_observers removeObjectForKey:object]; 559 } 560 [object removeObserver:self forKeyPath:path]; 561 } 562 } 563 } 564} 565 566- (void)_removeObservers 567{ 568 [self _tryRemoveObserver:_player forKeyPath:EXAVPlayerDataObserverStatusKeyPath]; 569 [self _removeObserversForPlayerItems]; 570 [self _tryRemoveObserver:_player forKeyPath:EXAVPlayerDataObserverRateKeyPath]; 571 [self _tryRemoveObserver:_player forKeyPath:EXAVPlayerDataObserverCurrentItemKeyPath]; 572 [self _tryRemoveObserver:_player forKeyPath:EXAVPlayerDataObserverTimeControlStatusPath]; 573} 574 575- (void)_removeTimeObserver 576{ 577 if (_timeObserver) { 578 [_player removeTimeObserver:_timeObserver]; 579 _timeObserver = nil; 580 } 581} 582 583- (void)_removeObserversForPlayerItems 584{ 585 for (AVPlayerItem *item in _items) { 586 [self _removeObserversForPlayerItem:item]; 587 } 588} 589 590- (void)_removeObserversForPlayerItem:(AVPlayerItem *)playerItem 591{ 592 [self _tryRemoveObserver:playerItem forKeyPath:EXAVPlayerDataObserverStatusKeyPath]; 593 594 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 595 if (_finishObserver) { 596 [center removeObserver:_finishObserver]; 597 _finishObserver = nil; 598 } 599 if (_playbackStalledObserver) { 600 [center removeObserver:_playbackStalledObserver]; 601 _playbackStalledObserver = nil; 602 } 603 604 [self _tryRemoveObserver:playerItem forKeyPath:EXAVPlayerDataObserverPlaybackBufferEmptyKeyPath]; 605 [self _tryRemoveObserver:playerItem forKeyPath:EXAVPlayerDataObserverMetadataKeyPath]; 606} 607 608- (void)_addObserversForPlayerItem:(AVPlayerItem *)playerItem 609{ 610 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 611 EX_WEAKIFY(self); 612 613 void (^didPlayToEndTimeObserverBlock)(NSNotification *note) = ^(NSNotification *note) { 614 EX_ENSURE_STRONGIFY(self); 615 __strong EXAV *strongEXAV = self.exAV; 616 if (strongEXAV) { 617 dispatch_async(strongEXAV.methodQueue, ^{ 618 EX_ENSURE_STRONGIFY(self); 619 [self _callStatusUpdateCallbackWithExtraFields:@{EXAVPlayerDataStatusDidJustFinishKeyPath: @(YES)}]; 620 // If the player is looping, we would only like to advance to next item (which is handled by actionAtItemEnd) 621 if (!self.isLooping) { 622 [self.player pause]; 623 self.shouldPlay = NO; 624 __strong EXAV *strongEXAVInner = self.exAV; 625 if (strongEXAVInner) { 626 [strongEXAVInner demoteAudioSessionIfPossible]; 627 } 628 } 629 }); 630 } 631 }; 632 633 _finishObserver = [center addObserverForName:AVPlayerItemDidPlayToEndTimeNotification 634 object:[_player currentItem] 635 queue:nil 636 usingBlock:didPlayToEndTimeObserverBlock]; 637 638 void (^playbackStalledObserverBlock)(NSNotification *note) = ^(NSNotification *note) { 639 EX_ENSURE_STRONGIFY(self); 640 [self _callStatusUpdateCallback]; 641 }; 642 643 _playbackStalledObserver = [center addObserverForName:AVPlayerItemPlaybackStalledNotification 644 object:[_player currentItem] 645 queue:nil 646 usingBlock:playbackStalledObserverBlock]; 647 [self _addObserver:playerItem forKeyPath:EXAVPlayerDataObserverPlaybackBufferEmptyKeyPath]; 648 [self _addObserver:playerItem forKeyPath:EXAVPlayerDataObserverStatusKeyPath]; 649 [self _addObserver:playerItem forKeyPath:EXAVPlayerDataObserverMetadataKeyPath]; 650} 651 652- (void)_updateTimeObserver 653{ 654 [self _removeTimeObserver]; 655 656 EX_WEAKIFY(self); 657 658 CMTime interval = CMTimeMakeWithSeconds(_progressUpdateIntervalMillis.floatValue / 1000.0, NSEC_PER_SEC); 659 660 void (^timeObserverBlock)(CMTime time) = ^(CMTime time) { 661 EX_ENSURE_STRONGIFY(self); 662 __strong EXAV *strongEXAV = self.exAV; 663 if (strongEXAV) { 664 dispatch_async(strongEXAV.methodQueue, ^{ 665 EX_ENSURE_STRONGIFY(self); 666 667 if (self && self.player.status == AVPlayerStatusReadyToPlay) { 668 self.currentPosition = time; // We keep track of _currentPosition to reset the AVPlayer in handleMediaServicesReset. 669 [self _callStatusUpdateCallback]; 670 } 671 }); 672 } 673 }; 674 675 _timeObserver = [_player addPeriodicTimeObserverForInterval:interval 676 queue:NULL 677 usingBlock:timeObserverBlock]; 678} 679 680- (void)_addObserversForNewPlayer 681{ 682 [self _removeObservers]; 683 [self _updateTimeObserver]; 684 685 [self _addObserver:_player forKeyPath:EXAVPlayerDataObserverRateKeyPath]; 686 NSUInteger currentItemObservationOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; 687 [self _addObserver:_player forKeyPath:EXAVPlayerDataObserverCurrentItemKeyPath options:currentItemObservationOptions]; 688 [self _addObserver:_player forKeyPath:EXAVPlayerDataObserverTimeControlStatusPath]; // Only available after iOS 10 689 [self _addObserversForPlayerItem:_player.currentItem]; 690} 691 692- (void)observeValueForKeyPath:(NSString *)keyPath 693 ofObject:(id)object 694 change:(NSDictionary *)change 695 context:(void *)context 696{ 697 if (_player == nil || (object != _player && ![_items containsObject:object])) { 698 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 699 return; 700 } 701 702 __weak EXAVPlayerData *weakSelf = nil; 703 704 // Specification of Objective-C always allows creation of weak references, 705 // however on iOS trying to create a weak reference to a deallocated object 706 // results in throwing an exception. If this happens we have nothing to do 707 // as the EXAVPlayerData is being deallocated, so let's early return. 708 // 709 // See Stackoverflow question: 710 // https://stackoverflow.com/questions/35991363/why-setting-object-that-is-undergoing-deallocation-to-weak-property-results-in-c#42329741 711 @try { 712 weakSelf = self; 713 } @catch (NSException *exception) { 714 return; 715 } 716 717 __strong EXAV *strongEXAV = _exAV; 718 if (strongEXAV == nil) { 719 return; 720 } 721 722 dispatch_async(strongEXAV.methodQueue, ^{ 723 __strong EXAVPlayerData *strongSelf = weakSelf; 724 if (strongSelf) { 725 if (object == strongSelf.player) { 726 if ([keyPath isEqualToString:EXAVPlayerDataObserverStatusKeyPath]) { 727 switch (strongSelf.player.status) { 728 case AVPlayerStatusUnknown: 729 break; 730 case AVPlayerStatusReadyToPlay: 731 if (!strongSelf.isLoaded && strongSelf.player.currentItem.status == AVPlayerItemStatusReadyToPlay) { 732 [strongSelf _finishLoadingNewPlayer]; 733 } 734 break; 735 case AVPlayerStatusFailed: { 736 strongSelf.isLoaded = NO; 737 NSString *errorMessage = [NSString stringWithFormat:@"The AVPlayer instance has failed with the error code %li and domain \"%@\".", (long) strongSelf.player.error.code, strongSelf.player.error.domain]; 738 if (strongSelf.player.error.localizedFailureReason) { 739 NSString *reasonMessage = [strongSelf.player.error.localizedFailureReason stringByAppendingString:@" - "]; 740 errorMessage = [reasonMessage stringByAppendingString:errorMessage]; 741 } 742 if (strongSelf.loadFinishBlock) { 743 strongSelf.loadFinishBlock(NO, nil, errorMessage); 744 strongSelf.loadFinishBlock = nil; 745 } else if (strongSelf.errorCallback) { 746 strongSelf.errorCallback(errorMessage); 747 } 748 break; 749 } 750 } 751 } else if ([keyPath isEqualToString:EXAVPlayerDataObserverRateKeyPath]) { 752 if (strongSelf.player.rate != 0) { 753 strongSelf.rate = @(strongSelf.player.rate); 754 } 755 // If replayResolve is not nil here, it means that we had to pause playback due to empty queue of rewinded items. 756 // This clause handles iOS 9. 757 if (strongSelf.player.rate > 0 && strongSelf.replayResolve) { 758 strongSelf.replayResolve([strongSelf getStatus]); 759 strongSelf.replayResolve = nil; 760 } 761 762 int observedRate = strongSelf.observedRate.floatValue * 1000; 763 int currentRate = strongSelf.player.rate * 1000; 764 765 if (abs(observedRate - currentRate) > 1) { 766 [strongSelf _callStatusUpdateCallback]; 767 strongSelf.observedRate = @(strongSelf.player.rate); 768 } 769 } else if ([keyPath isEqualToString:EXAVPlayerDataObserverTimeControlStatusPath]) { 770 bool statusChanged = strongSelf.player.timeControlStatus != strongSelf.timeControlStatus; 771 strongSelf.timeControlStatus = strongSelf.player.timeControlStatus; 772 if (statusChanged) { 773 [strongSelf _callStatusUpdateCallback]; 774 } 775 // If replayResolve is not nil here, it means that we had to pause playback due to empty queue of rewinded items. 776 // This clause handles iOS 10+. 777 if (strongSelf.timeControlStatus == AVPlayerTimeControlStatusPlaying && strongSelf.replayResolve) { 778 strongSelf.replayResolve([strongSelf getStatus]); 779 strongSelf.replayResolve = nil; 780 } 781 } else if ([keyPath isEqualToString:EXAVPlayerDataObserverCurrentItemKeyPath]) { 782 [strongSelf _removeObserversForPlayerItems]; 783 [strongSelf _addObserversForPlayerItem:change[NSKeyValueChangeNewKey]]; 784 // Treadmill pattern, see: https://developer.apple.com/videos/play/wwdc2016/503/ 785 AVPlayerItem *removedPlayerItem = change[NSKeyValueChangeOldKey]; 786 if (removedPlayerItem && removedPlayerItem != (id)[NSNull null]) { 787 // Observers may have been removed in _finishObserver or replayWithStatus:resolver:rejecter 788 789 // Rewind player item and re-add to queue 790 if (CMTimeCompare(removedPlayerItem.currentTime, kCMTimeZero) != 0) { 791 // In some cases (when using HSLS/m3u8 files), the completionHandler 792 // was not called after the stream had completed fully. 793 // This appears to be a bug in iOS. 794 // Therefore, do not wait for the seek to complete, but merely 795 // initiate the seek and expect it to have completed when it's 796 // this AVPlayerItem's turn to play. 797 [removedPlayerItem seekToTime:kCMTimeZero completionHandler:nil]; 798 } 799 [strongSelf.player insertItem:removedPlayerItem afterItem:nil]; 800 } 801 802 if (self.sampleBufferCallback != nil) { 803 // Tap is installed per item, so we re-install for the new item. 804 [self installTap]; 805 } 806 } 807 } else if (object == strongSelf.player.currentItem) { 808 if ([keyPath isEqualToString:EXAVPlayerDataObserverStatusKeyPath]) { 809 switch (strongSelf.player.currentItem.status) { 810 case AVPlayerItemStatusUnknown: 811 break; 812 case AVPlayerItemStatusReadyToPlay: 813 if (!strongSelf.isLoaded && strongSelf.player.status == AVPlayerItemStatusReadyToPlay) { 814 [strongSelf _finishLoadingNewPlayer]; 815 } 816 break; 817 case AVPlayerItemStatusFailed: { 818 NSString *errorMessage = [NSString stringWithFormat:@"The AVPlayerItem instance has failed with the error code %li and domain \"%@\".", (long) strongSelf.player.currentItem.error.code, strongSelf.player.currentItem.error.domain]; 819 if (strongSelf.player.currentItem.error.localizedFailureReason) { 820 NSString *reasonMessage = [strongSelf.player.currentItem.error.localizedFailureReason stringByAppendingString:@" - "]; 821 errorMessage = [reasonMessage stringByAppendingString:errorMessage]; 822 } 823 if (strongSelf.loadFinishBlock) { 824 strongSelf.loadFinishBlock(NO, nil, errorMessage); 825 strongSelf.loadFinishBlock = nil; 826 } else if (strongSelf.errorCallback) { 827 strongSelf.errorCallback(errorMessage); 828 } 829 strongSelf.isLoaded = NO; 830 break; 831 } 832 } 833 [strongSelf _callStatusUpdateCallback]; 834 } else if ([keyPath isEqualToString:EXAVPlayerDataObserverPlaybackBufferEmptyKeyPath]) { 835 [strongSelf _callStatusUpdateCallback]; 836 } else if ([keyPath isEqualToString:EXAVPlayerDataObserverMetadataKeyPath] && strongSelf.metadataUpdateCallback) { 837 NSArray<AVMetadataItem *> *timedMetadata = strongSelf.player.currentItem.timedMetadata; 838 NSMutableDictionary *metadata = [@{} mutableCopy]; 839 for (AVMetadataItem *item in timedMetadata) { 840 if ([item.commonKey isEqual:AVMetadataCommonKeyTitle]) { 841 NSString *title = item.stringValue; 842 [metadata setObject:title forKey:@"title"]; 843 break; 844 } 845 } 846 strongSelf.metadataUpdateCallback(metadata); 847 } 848 } 849 } 850 }); 851} 852 853#pragma mark - Sample Buffer Callbacks & AudioMix Tap 854 855- (void)setSampleBufferCallback:(EXAudioSampleCallback *)sampleBufferCallback 856{ 857 if (sampleBufferCallback) { 858 [self installTap]; 859 } else { 860 [self uninstallTap]; 861 } 862 863 _audioSampleBufferCallback = sampleBufferCallback; 864} 865 866- (EXAudioSampleCallback *)sampleBufferCallback 867{ 868 return _audioSampleBufferCallback; 869} 870 871- (void)installTap 872{ 873 AVPlayerItem *item = [_player currentItem]; 874 // TODO: What if a player item has multiple tracks? 875 AVAssetTrack *track = item.tracks.firstObject.assetTrack; 876 if (!track) 877 { 878 EXLogError(@"Failed to find a track in the current player item!"); 879 return; 880 } 881 882 AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix]; 883 if (audioMix) { 884 AVMutableAudioMixInputParameters *audioMixInputParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:track]; 885 if (audioMixInputParameters) { 886 MTAudioProcessingTapCallbacks callbacks; 887 888 callbacks.version = kMTAudioProcessingTapCallbacksVersion_0; 889 callbacks.clientInfo = (__bridge void *)self; 890 callbacks.init = EXTapInit; 891 callbacks.finalize = EXTapFinalize; 892 callbacks.prepare = EXTapPrepare; 893 callbacks.unprepare = EXTapUnprepare; 894 callbacks.process = EXTapProcess; 895 896 MTAudioProcessingTapRef audioProcessingTap; 897 OSStatus status = MTAudioProcessingTapCreate(kCFAllocatorDefault, &callbacks, kMTAudioProcessingTapCreationFlag_PreEffects, &audioProcessingTap); 898 if (status == noErr) { 899 audioMixInputParameters.audioTapProcessor = audioProcessingTap; 900 audioMix.inputParameters = @[audioMixInputParameters]; 901 902 [item setAudioMix:audioMix]; 903 904 CFRelease(audioProcessingTap); 905 } else { 906 EXLogError(@"Failed to create MTAudioProcessingTap!"); 907 } 908 } 909 } 910} 911 912- (void)uninstallTap 913{ 914 AVPlayerItem *item = [_player currentItem]; 915 [item setAudioMix:nil]; 916} 917 918#pragma mark - Audio Sample Buffer Callbacks (MTAudioProcessingTapCallbacks) 919 920void EXTapInit(MTAudioProcessingTapRef tap, void *clientInfo, void **tapStorageOut) 921{ 922 AVAudioTapProcessorContext *context = calloc(1, sizeof(AVAudioTapProcessorContext)); 923 924 // Initialize MTAudioProcessingTap context. 925 context->isNonInterleaved = false; 926 context->self = clientInfo; 927 928 *tapStorageOut = context; 929} 930 931void EXTapFinalize(MTAudioProcessingTapRef tap) 932{ 933 AVAudioTapProcessorContext *context = (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(tap); 934 935 // Clear MTAudioProcessingTap context. 936 context->self = NULL; 937 938 free(context); 939} 940 941void EXTapPrepare(MTAudioProcessingTapRef tap, CMItemCount maxFrames, const AudioStreamBasicDescription *processingFormat) 942{ 943 AVAudioTapProcessorContext *context = (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(tap); 944 945 context->supportedTapProcessingFormat = true; 946 947 if (processingFormat->mFormatID != kAudioFormatLinearPCM) { 948 EXLogInfo(@"Audio Format ID for audioProcessingTap: LinearPCM"); 949 // TODO(barthap): Does LinearPCM work with the audio sample buffer callback? 950 } 951 if (!(processingFormat->mFormatFlags & kAudioFormatFlagIsFloat)) { 952 EXLogInfo(@"Audio Format ID for audioProcessingTap: Float only"); 953 // TODO(barthap): Does Float work with the audio sample buffer callback? 954 } 955 956 if (processingFormat->mFormatFlags & kAudioFormatFlagIsNonInterleaved) { 957 context->isNonInterleaved = true; 958 } 959} 960 961void EXTapUnprepare(MTAudioProcessingTapRef tap) 962{ 963 AVAudioTapProcessorContext *context = 964 (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(tap); 965 context->self = NULL; 966} 967 968void EXTapProcess(MTAudioProcessingTapRef tap, CMItemCount numberFrames, MTAudioProcessingTapFlags flags, AudioBufferList *bufferListInOut, CMItemCount *numberFramesOut, MTAudioProcessingTapFlags *flagsOut) 969{ 970 AVAudioTapProcessorContext *context = 971 (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(tap); 972 973 if (!context->self) { 974 EXLogWarn(@"Audio Processing Tap has been destroyed!"); 975 return; 976 } 977 978 EXAVPlayerData *_self = ((__bridge EXAVPlayerData *)context->self); 979 980 if (!_self.sampleBufferCallback) { 981 return; 982 } 983 984 // Get actual audio buffers from MTAudioProcessingTap 985 OSStatus status = MTAudioProcessingTapGetSourceAudio(tap, numberFrames, bufferListInOut, flagsOut, NULL, numberFramesOut); 986 if (noErr != status) { 987 EXLogWarn(@"MTAudioProcessingTapGetSourceAudio: %d", (int)status); 988 return; 989 } 990 991 double seconds = [_self getCurrentPositionPrecise]; 992 [_self.sampleBufferCallback callWithAudioBuffer:&bufferListInOut->mBuffers[0] andTimestamp:seconds]; 993} 994 995#pragma mark - EXAVObject 996 997- (void)pauseImmediately 998{ 999 if (_player) { 1000 [_player pause]; 1001 } 1002} 1003 1004- (EXAVAudioSessionMode)getAudioSessionModeRequired 1005{ 1006 if (_player && ([self _isPlayerPlaying] || [self _shouldPlayerPlay])) { 1007 return _isMuted ? EXAVAudioSessionModeActiveMuted : EXAVAudioSessionModeActive; 1008 } 1009 return EXAVAudioSessionModeInactive; 1010} 1011 1012- (void)appDidForeground 1013{ 1014 [self _tryPlayPlayerWithRateAndMuteIfNecessary]; 1015} 1016 1017- (void)appDidBackgroundStayActive:(BOOL)stayActive 1018{ 1019 // EXAudio already forced pause. 1020} 1021 1022- (void)handleAudioSessionInterruption:(NSNotification*)notification 1023{ 1024 NSNumber *interruptionType = [[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey]; 1025 switch (interruptionType.unsignedIntegerValue) { 1026 case AVAudioSessionInterruptionTypeBegan: 1027 // System already forced pause. 1028 [self _callStatusUpdateCallback]; 1029 break; 1030 case AVAudioSessionInterruptionTypeEnded: 1031 [self _tryPlayPlayerWithRateAndMuteIfNecessary]; 1032 [self _callStatusUpdateCallback]; 1033 break; 1034 default: 1035 break; 1036 } 1037} 1038 1039- (void)handleMediaServicesReset:(void (^)(void))finishCallback 1040{ 1041 // See here: https://developer.apple.com/library/content/qa/qa1749/_index.html 1042 // (this is an unlikely notification to receive, but best practices suggests that we catch it just in case) 1043 1044 _isLoaded = NO; 1045 1046 // We want to temporarily disable _statusUpdateCallback so that all of the new state changes don't trigger a waterfall of updates: 1047 void (^callback)(NSDictionary *) = _statusUpdateCallback; 1048 _statusUpdateCallback = nil; 1049 1050 EX_WEAKIFY(self); 1051 _loadFinishBlock = ^(BOOL success, NSDictionary *successStatus, NSString *error) { 1052 EX_ENSURE_STRONGIFY(self); 1053 if (finishCallback != nil) { 1054 finishCallback(); 1055 } 1056 if (self.statusUpdateCallback == nil) { 1057 self.statusUpdateCallback = callback; 1058 } 1059 [self _callStatusUpdateCallback]; 1060 if (!success && self.errorCallback) { 1061 self.errorCallback(error); 1062 } 1063 }; 1064 1065 [self _removeTimeObserver]; 1066 [self _removeObservers]; 1067 1068 [self _loadNewPlayer]; 1069} 1070 1071#pragma mark - NSObject Lifecycle 1072 1073/* 1074 * Call this synchronously on the main thread to remove the EXAVPlayerData 1075 * as an observer before KVO messages are broadcasted on the main thread. 1076 */ 1077- (void)cleanup 1078{ 1079 // this triggers the audio tap removal 1080 [self setSampleBufferCallback:nil]; 1081 [self _removeTimeObserver]; 1082 [self _removeObservers]; 1083} 1084 1085- (void)dealloc 1086{ 1087 [self cleanup]; 1088} 1089 1090# pragma mark - Utilities 1091 1092/* 1093 * For a given NSDictionary returns a new NSDictionary with 1094 * entries only of type (String, String). 1095 */ 1096- (NSDictionary *)validatedRequestHeaders:(NSDictionary *)requestHeaders 1097{ 1098 NSMutableDictionary *validatedHeaders = [NSMutableDictionary new]; 1099 for (id key in requestHeaders.allKeys) { 1100 id value = requestHeaders[key]; 1101 if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) { 1102 validatedHeaders[key] = value; 1103 } 1104 } 1105 return validatedHeaders; 1106} 1107 1108@end 1109