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