Skip to content

Instantly share code, notes, and snippets.

@kosso
Last active August 23, 2018 21:44
Show Gist options
  • Save kosso/5fcd7ded25b0cc8ec932db633f995053 to your computer and use it in GitHub Desktop.
Save kosso/5fcd7ded25b0cc8ec932db633f995053 to your computer and use it in GitHub Desktop.
// From SDK mobilesdk-7.5.0.v20180821233900-osx
// Fixes new Ti.Media.audioPlayer issues (for me)
// Search for 'Kosso' to see changes.
// - Removed timedmetadata observer when setting new url.
// - Added ability to see when duration is available.
// - Fixed duration property return value.
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2009-2018 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
#ifdef USE_TI_MEDIAAUDIOPLAYER
#import <AVKit/AVKit.h>
#import "TiMediaAudioPlayerProxy.h"
#import "TiMediaAudioSession.h"
#import "TiUtils.h"
@implementation TiMediaAudioPlayerProxy
#pragma mark Internal
- (void)_initWithProperties:(NSDictionary *)properties
{
[super _initWithProperties:properties];
_url = [TiUtils toURL:[properties objectForKey:@"url"] proxy:self];
}
- (void)_destroy
{
if (_state == TiAudioPlayerStatePlaying || _state == TiAudioPlayerStatePaused) {
[self stop:nil];
}
[self removeNotificationObserver];
[[NSNotificationCenter defaultCenter] removeObserver:self];
_player = nil;
[super _destroy];
}
- (NSString *)apiName
{
return @"Ti.Media.AudioPlayer";
}
- (void)_listenerAdded:(NSString *)type count:(int)count
{
if (count == 1 && [type isEqualToString:@"progress"]) {
__weak TiMediaAudioPlayerProxy *weakSelf = self;
_timeObserver = [[self player] addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0, NSEC_PER_SEC)
queue:nil
usingBlock:^(CMTime time) {
TiMediaAudioPlayerProxy *strongSelf = weakSelf;
[strongSelf fireEvent:@"progress"
withObject:@{
@"progress" : NUMINT(CMTimeGetSeconds(time) * 1000)
}];
}];
}
}
- (void)_listenerRemoved:(NSString *)type count:(int)count
{
if (count == 0 && [type isEqualToString:@"progress"]) {
[[self player] removeTimeObserver:_timeObserver];
_timeObserver = nil;
}
}
- (AVPlayer *)player
{
if (_player == nil) {
if (_url == nil) {
[self throwException:NSLocalizedString(@"Invalid URL passed to the audio-player", nil)
subreason:NSLocalizedString(@"The \"url\" probably has not been set to a valid value.", nil)
location:CODELOCATION];
}
_player = [AVPlayer playerWithURL:_url];
[self addNotificationObserver];
_state = TiAudioPlayerStateInitialized;
}
return _player;
}
#pragma mark Deprecated APIs
- (void)setPaused:(NSNumber *)paused
{
DEPRECATED_REPLACED(@"Media.AudioPlayer.setPaused", @"7.3.0", @"Media.AudioPlayer.pause");
if ([TiUtils boolValue:paused]) {
[[self player] pause];
} else {
[[self player] play];
}
}
- (void)play:(id)unused
{
DEPRECATED_REPLACED(@"Media.AudioPlayer.play", @"7.3.0", @"Media.AudioPlayer.start");
[self start:unused];
}
#pragma mark Public APIs
- (NSNumber *)waiting
{
return NUMBOOL(_state == TiAudioPlayerStateWaitingForQueueToStart || _state == TiAudioPlayerStateBuffering);
}
- (NSNumber *)idle
{
return NUMBOOL(_state == TiAudioPlayerStateInitialized);
}
- (NSNumber *)playing
{
return NUMBOOL(_state == TiAudioPlayerStatePlaying);
}
- (NSNumber *)buffering
{
return NUMBOOL(_state == TiAudioPlayerStateBuffering);
}
- (NSNumber *)bitRate
{
return NUMFLOAT([[self player] rate]);
}
- (NSNumber *)progress
{
return NUMDOUBLE(CMTimeGetSeconds([[self player] currentTime]) * 1000);
}
- (NSNumber *)state
{
return NUMDOUBLE(_state);
}
- (NSNumber *)duration
{
// Kosso
// If duration is kCMTimeIndefinite this means it's a live stream.
// Note: The duration availability is now obvserved using:
// [[[self player] currentItem].asset addObserver:self forKeyPath:@"duration" options:NSKeyValueObservingOptionInitial context:nil];
if( CMTimeCompare([[self player] currentItem].asset.duration, kCMTimeIndefinite) == 0){
// previously:
//if (CMTimeGetSeconds([[[self player] currentItem] duration]) == CMTimeGetSeconds(kCMTimeIndefinite)) {
_duration = 0;
} else {
// Convert duration to milliseconds (parity with progress/Android)
// _duration = (int)(CMTimeGetSeconds([[[self player] currentItem] duration]) * 1000);
// Kosso
_duration = (int)(CMTimeGetSeconds([[self player] currentItem].asset.duration) * 1000);
}
// Kosso added. To expose value to e.source.duration in events.
return NUMINT(_duration);
}
- (NSNumber *)paused
{
return NUMBOOL(_state == TiAudioPlayerStatePaused);
}
- (NSNumber *)volume
{
return NUMFLOAT([[self player] volume]);
}
- (void)setVolume:(NSNumber *)volume
{
[[self player] setVolume:[TiUtils floatValue:volume def:1.0]];
}
- (void)setBufferSize:(NSNumber *)bufferSize
{
[[[self player] currentItem] setPreferredForwardBufferDuration:[bufferSize doubleValue] * 1000];
}
- (void)setAllowsExternalPlayback:(NSNumber *)allowsExternalPlayback
{
[[self player] setAllowsExternalPlayback:[TiUtils boolValue:allowsExternalPlayback]];
}
- (NSNumber *)allowsExternalPlayback
{
return NUMBOOL([[self player] allowsExternalPlayback]);
}
- (void)setRate:(NSNumber *)rate
{
[[self player] setRate:[TiUtils floatValue:rate]];
}
- (NSNumber *)rate
{
return NUMFLOAT([[self player] rate]);
}
- (void)setMuted:(NSNumber *)muted
{
[[self player] setMuted:[TiUtils boolValue:muted]];
}
- (NSNumber *)muted
{
return NUMBOOL([[self player] isMuted]);
}
- (void)externalPlaybackActive
{
return NUMBOOL([[self player] isExternalPlaybackActive]);
}
- (NSNumber *)bufferSize
{
return NUMDOUBLE([[[self player] currentItem] preferredForwardBufferDuration]);
}
- (void)setUrl:(id)url
{
if (![NSThread isMainThread]) {
TiThreadPerformOnMainThread(^{
[self setUrl:url];
},
YES);
return;
}
ENSURE_SINGLE_ARG(url, NSString);
_url = [TiUtils toURL:url proxy:self];
// Properly clean up old observer before changing player item
if (_player != nil) {
// Remove old KVO-observer
[self removeNotificationObserver];
// Change player item
[[self player] replaceCurrentItemWithPlayerItem:[AVPlayerItem playerItemWithURL:_url]];
// Add new KVO-observer
[self addNotificationObserver];
// Restart (stop -> start) player
[self restart:nil];
}
}
- (NSString *)url
{
return [_url absoluteString];
}
- (void)seekToTime:(id)time
{
ENSURE_SINGLE_ARG(time, NSNumber);
if (_player == nil) {
return;
}
float formattedTime = [TiUtils floatValue:time] / 1000;
__weak TiMediaAudioPlayerProxy *weakSelf = self;
[_player seekToTime:CMTimeMake(formattedTime, 1)
completionHandler:^(BOOL finished) {
TiMediaAudioPlayerProxy *strongSelf = weakSelf;
if ([strongSelf _hasListeners:@"seek"]) {
[strongSelf fireEvent:@"seek" withObject:@{ @"finished" : NUMBOOL(finished) }];
}
}];
}
- (void)start:(id)unused
{
if (![NSThread isMainThread]) {
TiThreadPerformOnMainThread(^{
[self start:unused];
},
YES);
return;
}
_state = TiAudioPlayerStateStartingFileThread;
// indicate we're going to start playing
if (![[TiMediaAudioSession sharedSession] canPlayback]) {
_state = TiAudioPlayerStateStopped;
[self throwException:@"Improper audio session mode for playback"
subreason:[[TiMediaAudioSession sharedSession] sessionMode]
location:CODELOCATION];
}
if (_player == nil || !(_state == TiAudioPlayerStatePlaying || _state == TiAudioPlayerStatePaused)) {
[[TiMediaAudioSession sharedSession] startAudioSession];
}
[[self player] play];
}
- (void)restart:(id)args
{
[self stop:nil];
[self start:nil];
}
- (void)stop:(id)unused
{
if (![NSThread isMainThread]) {
TiThreadPerformOnMainThread(^{
[self stop:unused];
},
YES);
return;
}
_state = TiAudioPlayerStateStopping;
[[self player] pause];
[[self player] seekToTime:kCMTimeZero];
if ([[TiMediaAudioSession sharedSession] isActive]) {
[[TiMediaAudioSession sharedSession] stopAudioSession];
}
}
- (void)pause:(id)unused
{
if (![NSThread isMainThread]) {
TiThreadPerformOnMainThread(^{
[self pause:unused];
},
YES);
return;
}
[[self player] pause];
}
- (void)release:(id)unused
{
[self stop:nil];
_player = nil;
}
- (NSString *)stateDescription:(id)state
{
ENSURE_SINGLE_ARG(state, NSNumber);
return [TiMediaAudioPlayerProxy _stateToString:[TiUtils intValue:state]];
}
#pragma mark Utilities
+ (NSString *)_stateToString:(NSInteger)state
{
switch (state) {
case TiAudioPlayerStateInitialized:
return NSLocalizedString(@"initialized", nil);
case TiAudioPlayerStateStartingFileThread:
return NSLocalizedString(@"starting", nil);
case TiAudioPlayerStateWaitingForData:
return NSLocalizedString(@"waiting_for_data", nil);
case TiAudioPlayerStateWaitingForQueueToStart:
return NSLocalizedString(@"waiting_for_queue", nil);
case TiAudioPlayerStatePlaying:
return NSLocalizedString(@"playing", nil);
case TiAudioPlayerStateBuffering:
return NSLocalizedString(@"buffering", nil);
case TiAudioPlayerStateStopping:
return NSLocalizedString(@"stopping", nil);
case TiAudioPlayerStateStopped:
return NSLocalizedString(@"stopped", nil);
case TiAudioPlayerStatePaused:
return NSLocalizedString(@"paused", nil);
}
return NSLocalizedString(@"unknown", nil);
}
#pragma mark Observer
- (void)addNotificationObserver
{
WARN_IF_BACKGROUND_THREAD; //NSNotificationCenter is not threadsafe!
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
// The AVPlayer does not properly support state management on iOS < 10.
// Remove this once we bump the minimum iOS version to 10+.
if ([TiUtils isIOS10OrGreater]) {
// iOS 10+: For playbackState property / playbackstate event
[[self player] addObserver:self forKeyPath:@"timeControlStatus" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
} else {
// iOS < 10: For playbackstate event
[[self player] addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
// For "error" event
[nc addObserver:self selector:@selector(handlePlayerErrorNotification:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:_player.currentItem];
// For "complete" event
[nc addObserver:self selector:@selector(handlePlayerCompleteNotification:) name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem];
// Buffering
[[[self player] currentItem] addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
[[[self player] currentItem] addObserver:self forKeyPath:@"playbackBufferFull" options:NSKeyValueObservingOptionNew context:nil];
// Timed metadata
[[[self player] currentItem] addObserver:self forKeyPath:@"timedMetadata" options:NSKeyValueObservingOptionNew context:nil];
// Kosso
// Duration available
// This can provide a similar event to HTML5 Audio's 'durationchange' event.
[[[self player] currentItem].asset addObserver:self forKeyPath:@"duration" options:NSKeyValueObservingOptionInitial context:nil];
}
- (void)removeNotificationObserver
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:nil];
[nc removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
if (_player == nil) {
return;
}
if ([TiUtils isIOS10OrGreater]) {
[[self player] removeObserver:self forKeyPath:@"timeControlStatus"];
} else {
[[self player] removeObserver:self forKeyPath:@"rate"];
}
[[[self player] currentItem] removeObserver:self forKeyPath:@"playbackBufferEmpty"];
[[[self player] currentItem] removeObserver:self forKeyPath:@"playbackBufferFull"];
// Kosso
[[[self player] currentItem] removeObserver:self forKeyPath:@"timedMetadata"]; // was missing.
[[[self player] currentItem].asset removeObserver:self forKeyPath:@"duration"]; // added
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context
{
if ([TiUtils isIOS10OrGreater]) {
if (object == _player && [keyPath isEqualToString:@"timeControlStatus"]) {
[self handleTimeControlStatusNotification:nil];
}
} else {
if (object == _player && [keyPath isEqualToString:@"rate"]) {
[self handlePlaybackStateChangeNotification:nil];
}
}
if (object == _player.currentItem && [keyPath isEqualToString:@"playbackBufferEmpty"]) {
_state = TiAudioPlayerStateBuffering;
}
if (object == _player.currentItem && [keyPath isEqualToString:@"playbackBufferFull"]) {
_state = TiAudioPlayerStateWaitingForQueueToStart;
}
if (object == _player.currentItem && [keyPath isEqualToString:@"timedMetadata"]) {
[self handleTimedMetadataNotification:_player.currentItem];
}
// Kosso
if (object == _player.currentItem.asset && [keyPath isEqualToString:@"duration"]) {
// NSLog(@"[INFO] duration is available");
if( CMTimeCompare(_player.currentItem.asset.duration, kCMTimeIndefinite) == 0){
// NSLog(@"[INFO] _player.currentItem.asset.duration is infinite. Live stream.");
} else {
NSString *timeDesc = (NSString *)CFBridgingRelease(CMTimeCopyDescription(NULL, _player.currentItem.asset.duration));
// NSLog(@"[INFO] _player DURATION timeDesc: %@", timeDesc); // eg: {13156992/44100 = 298.344}
// NSLog(@"[INFO] duration: %d", (int)(CMTimeGetSeconds([[[[self player] currentItem] asset] duration]) * 1000 ));
}
}
}
// iOS < 10
- (void)handlePlaybackStateChangeNotification:(NSNotification *)note
{
TiAudioPlayerState oldState = _state;
switch (_player.status) {
case AVPlayerStatusUnknown:
case AVPlayerStatusFailed:
_state = TiAudioPlayerStateStopped;
break;
case AVPlayerStatusReadyToPlay:
if (_player.rate == 1.0) {
_state = TiAudioPlayerStatePlaying;
} else if (_player.currentItem.currentTime.value == 0 || oldState == TiAudioPlayerStateStopping) {
_state = TiAudioPlayerStateStopped;
} else {
_state = TiAudioPlayerStatePaused;
}
break;
}
if ([self _hasListeners:@"change"] && oldState != _state) {
[self fireEvent:@"change"
withObject:@{
@"state" : NUMINTEGER(_state),
@"description" : [TiMediaAudioPlayerProxy _stateToString:_state]
}];
}
}
// iOS 10+
- (void)handleTimeControlStatusNotification:(NSNotification *)note
{
TiAudioPlayerState oldState = _state;
if (_player.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
_state = TiAudioPlayerStatePlaying;
} else if (_player.timeControlStatus == AVPlayerTimeControlStatusPaused) {
if (_player.currentItem.currentTime.value == 0.0 || oldState == TiAudioPlayerStateStopping) {
_state = TiAudioPlayerStateStopped;
} else {
_state = TiAudioPlayerStatePaused;
}
} else if (_player.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate) {
_state = TiAudioPlayerStateWaitingForQueueToStart;
}
if ([self _hasListeners:@"change"] && oldState != _state) {
[self fireEvent:@"change"
withObject:@{
@"state" : NUMINTEGER(_state),
@"description" : [TiMediaAudioPlayerProxy _stateToString:_state]
}];
}
}
#pragma mark Events
- (void)handlePlayerErrorNotification:(NSNotification *)note
{
NSError *error = note.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey];
_state = TiAudioPlayerStateStopped;
if ([self _hasListeners:@"error"]) {
[self fireEvent:@"error" withObject:@{ @"error" : error.localizedDescription }];
}
}
- (void)handleTimedMetadataNotification:(AVPlayerItem *)playerItem
{
if (![self _hasListeners:@"metadata"]) {
return;
}
NSMutableArray *result = [NSMutableArray arrayWithCapacity:playerItem.timedMetadata.count];
for (AVMetadataItem *metadata in playerItem.timedMetadata) {
[result addObject:@{
@"key" : metadata.key,
@"keySpace" : metadata.keySpace,
@"value" : metadata.value,
@"extraAttributes" : metadata.extraAttributes
}];
}
[self fireEvent:@"metadata" withObject:@{ @"items" : result }];
}
- (void)handlePlayerCompleteNotification:(NSNotification *)note
{
if ([self _hasListeners:@"complete"]) {
NSMutableDictionary *event = [NSMutableDictionary dictionaryWithObjectsAndKeys:NUMBOOL(_player.error == nil), @"success", nil];
if (_player.error != nil) {
[event setObject:_player.error.localizedDescription forKey:@"error"];
[event setObject:NUMINTEGER(_player.error.code) forKey:@"code"];
}
[self fireEvent:@"complete" withObject:event];
}
}
#pragma mark Constants
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_INITIALIZED, TiAudioPlayerStateInitialized, @"Media.AudioPlayer.STATE_INITIALIZED", @"7.3.0", @"Media.AUDIO_STATE_INITIALIZED");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_STARTING, TiAudioPlayerStateStartingFileThread, @"Media.AudioPlayer.STATE_STARTING", @"7.3.0", @"Media.STATE_STARTING");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_WAITING_FOR_DATA, TiAudioPlayerStateWaitingForData, @"Media.AudioPlayer.STATE_WAITING_FOR_DATA", @"7.3.0", @"Media.AUDIO_STATE_WAITING_FOR_DATA");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_WAITING_FOR_QUEUE, TiAudioPlayerStateWaitingForQueueToStart, @"Media.AudioPlayer.STATE_WAITING_FOR_QUEUE", @"7.3.0", @"Media.AUDIO_STATE_WAITING_FOR_QUEUE");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_PLAYING, TiAudioPlayerStatePlaying, @"Media.AudioPlayer.STATE_PLAYING", @"7.3.0", @"Media.AUDIO_STATE_PLAYING");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_BUFFERING, TiAudioPlayerStateBuffering, @"Media.AudioPlayer.STATE_BUFFERING", @"7.3.0", @"Media.AUDIO_STATE_BUFFERING");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_STOPPING, TiAudioPlayerStateStopping, @"Media.AudioPlayer.STATE_STOPPING", @"7.3.0", @"Media.AUDIO_STATE_STOPPING");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_STOPPED, TiAudioPlayerStateStopped, @"Media.AudioPlayer.STATE_STOPPED", @"7.3.0", @"Media.AUDIO_STATE_STOPPED");
MAKE_SYSTEM_PROP_DEPRECATED_REPLACED(STATE_PAUSED, TiAudioPlayerStatePaused, @"Media.AudioPlayer.STATE_PAUSED", @"7.3.0", @"Media.AUDIO_STATE_PAUSED");
@end
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment