~zanneth/StreamPlayer

031a8be25654e3c14895b05bb1b931e91effc0da — Charles Magahern 7 years ago 7c20b18
Delegate callbacks for playback state
M StreamPlayer/Resources/Base.lproj/MainMenu.xib => StreamPlayer/Resources/Base.lproj/MainMenu.xib +13 -3
@@ 691,7 691,17 @@
                            <font key="font" metaFont="system"/>
                        </buttonCell>
                        <connections>
                            <action selector="_buttonPressed:" target="z9U-54-IbD" id="dNd-3e-5gS"/>
                            <action selector="_playPauseButtonPressed:" target="z9U-54-IbD" id="mj3-4d-muD"/>
                        </connections>
                    </button>
                    <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WZS-6A-Deu">
                        <rect key="frame" x="95" y="13" width="81" height="32"/>
                        <buttonCell key="cell" type="push" title="Stop" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="dJp-qF-9O1">
                            <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                            <font key="font" metaFont="system"/>
                        </buttonCell>
                        <connections>
                            <action selector="_stopButtonPressed:" target="z9U-54-IbD" id="HhZ-VV-Csf"/>
                        </connections>
                    </button>
                </subviews>


@@ 700,9 710,9 @@
        </window>
        <viewController id="z9U-54-IbD" customClass="MainViewController">
            <connections>
                <outlet property="_actionButton" destination="FWg-TU-hvG" id="Gbb-r5-Rg3"/>
                <outlet property="_urlTextField" destination="HGq-eh-5yw" id="Aah-Rk-1lW"/>
                <outlet property="actionButton" destination="FWg-TU-hvG" id="QlM-Xf-hUk"/>
                <outlet property="playPauseButton" destination="FWg-TU-hvG" id="od2-bq-hOb"/>
                <outlet property="stopButton" destination="WZS-6A-Deu" id="sGR-yJ-1GQ"/>
                <outlet property="urlTextField" destination="HGq-eh-5yw" id="g28-JQ-Cjh"/>
                <outlet property="view" destination="EiT-Mj-1SZ" id="m1G-rq-Fs4"/>
            </connections>

M StreamPlayer/Source/MainViewController.m => StreamPlayer/Source/MainViewController.m +25 -5
@@ 11,10 11,11 @@

static NSString * const MVCSavedURLStringDefaultsKey = @"SavedURLString";

@interface MainViewController ()
@interface MainViewController () <ZANStreamPlayerDelegate>

@property (weak) IBOutlet NSTextField *urlTextField;
@property (weak) IBOutlet NSButton *actionButton;
@property (weak) IBOutlet NSButton *playPauseButton;
@property (weak) IBOutlet NSButton *stopButton;

@end



@@ 34,9 35,22 @@ static NSString * const MVCSavedURLStringDefaultsKey = @"SavedURLString";
    }
}

#pragma mark - ZANStreamPlayerDelegate

- (void)streamPlayerPlaybackStateDidChange:(ZANStreamPlayer *)player
{
    if (player.playing) {
        _playPauseButton.title = @"Pause";
        _stopButton.enabled = YES;
    } else {
        _playPauseButton.title = @"Play";
        _stopButton.enabled = NO;
    }
}

#pragma mark - Actions

- (IBAction)_buttonPressed:(id)sender
- (IBAction)_playPauseButtonPressed:(id)sender
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:self.urlTextField.stringValue forKey:MVCSavedURLStringDefaultsKey];


@@ 45,14 59,20 @@ static NSString * const MVCSavedURLStringDefaultsKey = @"SavedURLString";
    if (!_player) {
        NSURL *url = [[NSURL alloc] initWithString:self.urlTextField.stringValue];
        _player = [[ZANStreamPlayer alloc] initWithURL:url];
        _player.delegate = self;
    }
    
    if (_player.playing) {
        [_player pause];
        _actionButton.title = @"Play";
    } else {
        [_player play];
        _actionButton.title = @"Pause";
    }
}

- (IBAction)_stopButtonPressed:(id)sender
{
    if (_player) {
        [_player stop];
    }
}


M StreamPlayer/Source/ZANStreamPlayer.h => StreamPlayer/Source/ZANStreamPlayer.h +15 -0
@@ 10,15 10,30 @@

NS_ASSUME_NONNULL_BEGIN

@class ZANStreamPlayer;

@protocol ZANStreamPlayerDelegate <NSObject>
@optional

- (void)streamPlayerPlaybackStateDidChange:(ZANStreamPlayer *)player;

@end

// -----------------------------------------------------------------------------

@interface ZANStreamPlayer : NSObject

@property (nonatomic, weak) id<ZANStreamPlayerDelegate> delegate;

@property (nonatomic, readonly) NSURL *url;
@property (nonatomic, readonly, getter=isPlaying) BOOL playing;
@property (nonatomic, readonly, getter=isStopped) BOOL stopped;

- (instancetype)initWithURL:(NSURL *)url;

- (void)play;
- (void)pause;
- (void)stop;

@end


M StreamPlayer/Source/ZANStreamPlayer.m => StreamPlayer/Source/ZANStreamPlayer.m +134 -32
@@ 18,6 18,13 @@
#define AUDIO_QUEUE_BUFFERS_COUNT   16
#define MAX_PACKET_DESCRIPTIONS     512

typedef NS_ENUM(NSUInteger, ZANStreamPlaybackState)
{
    ZANStreamPlaybackStateStopped = 0,
    ZANStreamPlaybackStatePaused,
    ZANStreamPlaybackStatePlaying,
};

static void _ZANPropertyListenerCallback(void *clientData,
                                         AudioFileStreamID fileStream,
                                         AudioFileStreamPropertyID propertyID,


@@ 29,11 36,18 @@ static void _ZANPacketsAvailableCallback(void *clientData,
                                         const void *data,
                                         AudioStreamPacketDescription *packetDescriptions);

static void _ZANAudioQueuePropertyListenerCallback(void *clientData,
                                                   AudioQueueRef audioQueue,
                                                   AudioQueuePropertyID propertyID);

static void _ZANAudioQueueOutputCallback(void *clientData,
                                         AudioQueueRef audioQueue,
                                         AudioQueueBufferRef buffer);

@interface ZANStreamPlayer () <NSURLSessionDataDelegate>

@property (nonatomic, assign) ZANStreamPlaybackState playbackState;

@end

@implementation ZANStreamPlayer


@@ 89,41 103,89 @@ static void _ZANAudioQueueOutputCallback(void *clientData,
    [_urlSession invalidateAndCancel];
}

#pragma mark - Accessors

- (BOOL)isPlaying
{
    return (self.playbackState == ZANStreamPlaybackStatePlaying);
}

- (BOOL)isStopped
{
    return (self.playbackState == ZANStreamPlaybackStateStopped);
}

#pragma mark - API

- (void)play
{
    _playing = YES;
    
    if (_audioQueue) {
        // reset so that we don't hear previously queued buffers
        OSStatus status = AudioQueueReset(_audioQueue);
        if (status != noErr) {
            [self _logError:@"Failed to reset audio queue (OSStatus = %d)", status];
            [self _handleError:[self _errorFromOSStatus:status]];
        }
    }
    
    if (!_dataTask) {
        [self _openReadStream];
        // the first couple bytes encountered will start the audio queue again
    }
    self.playbackState = ZANStreamPlaybackStatePlaying;
}

- (void)pause
{
    _playing = NO;
    self.playbackState = ZANStreamPlaybackStatePaused;
}

- (void)stop
{
    self.playbackState = ZANStreamPlaybackStateStopped;
}

#pragma mark - Private

- (void)setPlaybackState:(ZANStreamPlaybackState)playbackState
{
    _playbackState = playbackState;
    
    if (_audioQueue) {
        OSStatus status = AudioQueuePause(_audioQueue);
        if (status != noErr) {
            [self _logError:@"Failed to pause audio queue (OSStatus = %d)", status];
            [self _handleError:[self _errorFromOSStatus:status]];
        }
    switch (playbackState) {
        case ZANStreamPlaybackStatePlaying: {
            if (_audioQueue) {
                // reset so that we don't hear previously queued buffers
                OSStatus status = AudioQueueReset(_audioQueue);
                if (status != noErr) {
                    [self _logError:@"Failed to reset audio queue (OSStatus = %d)", status];
                    [self _handleError:[self _errorFromOSStatus:status]];
                }
            }
            
            if (!_dataTask) {
                [self _openReadStream];
                // the first couple bytes encountered will start the audio queue again
            }
        } break;
        
        case ZANStreamPlaybackStatePaused: {
            if (_audioQueue) {
                OSStatus status = AudioQueuePause(_audioQueue);
                if (status != noErr) {
                    [self _logError:@"Failed to pause audio queue (OSStatus = %d)", status];
                    [self _handleError:[self _errorFromOSStatus:status]];
                }
            }
            
            if (_dataTask) {
                [self _closeReadStream];
            }
        } break;
            
        case ZANStreamPlaybackStateStopped: {
            if (_audioQueue) {
                OSStatus status = AudioQueueStop(_audioQueue, true);
                if (status != noErr) {
                    [self _logError:@"Failed to stop audio queue (OSStatus = %d)", status];
                    [self _handleError:[self _errorFromOSStatus:status]];
                }
            }
            
            if (_dataTask) {
                [self _closeReadStream];
            }
        } break;
    }
    
    if (_dataTask) {
        [self _closeReadStream];
    if ([_delegate respondsToSelector:@selector(streamPlayerPlaybackStateDidChange:)]) {
        [_delegate streamPlayerPlaybackStateDidChange:self];
    }
}



@@ 256,6 318,13 @@ static void _ZANAudioQueueOutputCallback(void *clientData,
            break;
        }
        
        // setup property change handler
        status = AudioQueueAddPropertyListener(_audioQueue, kAudioQueueProperty_IsRunning, _ZANAudioQueuePropertyListenerCallback, (__bridge void *)self);
        if (status != noErr) {
            [self _logError:@"Failed to setup property change listener on audio queue (OSStatus = %d)", status];
            break;
        }
        
        // get the packet size, if available
        UInt32 packetBufferSize = 0;
        UInt32 propertySize = sizeof(UInt32);


@@ 325,7 394,7 @@ static void _ZANAudioQueueOutputCallback(void *clientData,
        }
        
        // start the audio queue processing hardware if necessary
        if (_playing) {
        if ([self isPlaying]) {
            status = AudioQueueStart(_audioQueue, NULL);
            if (status != noErr) {
                [self _logError:@"Failed to start audio queue hardware (OSStatus = %d)", status];


@@ 475,6 544,23 @@ static void _ZANAudioQueueOutputCallback(void *clientData,
    }
}

- (void)_handleAudioQueue:(AudioQueueRef)queue propertyDidChange:(AudioQueuePropertyID)property
{
    NSAssert(queue == _audioQueue, @"Incorrect audio queue input for property change");
    
    if (property == kAudioQueueProperty_IsRunning) {
        UInt32 isRunning = 0;
        UInt32 propertySize = sizeof(UInt32);
        AudioQueueGetProperty(_audioQueue, kAudioQueueProperty_IsRunning, &isRunning, &propertySize);
        
        [self _logMessage:@"Received IsRunning property state change for queue %p. New value = %u", queue, isRunning];
        
        if (self.playing && !isRunning) {
            self.playbackState = ZANStreamPlaybackStateStopped;
        }
    }
}

- (void)_handleBufferCompleteFromQueue:(AudioQueueRef)queue buffer:(AudioQueueBufferRef)buffer
{
    NSInteger bufferIdx = NSNotFound;


@@ 534,8 620,10 @@ void _ZANPropertyListenerCallback(void *clientData,
                                  AudioFileStreamPropertyID propertyID,
                                  UInt32 *flags)
{
    ZANStreamPlayer *player = (__bridge ZANStreamPlayer *)clientData;
    [player _handlePropertyChangeForFileStream:fileStream withPropertyID:propertyID flags:flags];
    @autoreleasepool {
        ZANStreamPlayer *player = (__bridge ZANStreamPlayer *)clientData;
        [player _handlePropertyChangeForFileStream:fileStream withPropertyID:propertyID flags:flags];
    }
}

void _ZANPacketsAvailableCallback(void *clientData,


@@ 544,15 632,29 @@ void _ZANPacketsAvailableCallback(void *clientData,
                                  const void *data,
                                  AudioStreamPacketDescription *packetDescriptions)
{
    ZANStreamPlayer *player = (__bridge ZANStreamPlayer *)clientData;
    NSData *audioData = [NSData dataWithBytesNoCopy:(void *)data length:bytesLength freeWhenDone:NO];
    [player _handleAudioPacketsAvailableWithData:audioData packetsCount:packetsCount packetDescriptions:packetDescriptions];
    @autoreleasepool {
        ZANStreamPlayer *player = (__bridge ZANStreamPlayer *)clientData;
        NSData *audioData = [NSData dataWithBytesNoCopy:(void *)data length:bytesLength freeWhenDone:NO];
        [player _handleAudioPacketsAvailableWithData:audioData packetsCount:packetsCount packetDescriptions:packetDescriptions];
    }
}

void _ZANAudioQueuePropertyListenerCallback(void *clientData,
                                            AudioQueueRef audioQueue,
                                            AudioQueuePropertyID propertyID)
{
    @autoreleasepool {
        ZANStreamPlayer *player = (__bridge ZANStreamPlayer *)clientData;
        [player _handleAudioQueue:audioQueue propertyDidChange:propertyID];
    }
}

void _ZANAudioQueueOutputCallback(void *clientData, AudioQueueRef audioQueue, AudioQueueBufferRef buffer)
{
    ZANStreamPlayer *player = (__bridge ZANStreamPlayer *)clientData;
    [player _handleBufferCompleteFromQueue:audioQueue buffer:buffer];
    @autoreleasepool {
        ZANStreamPlayer *player = (__bridge ZANStreamPlayer *)clientData;
        [player _handleBufferCompleteFromQueue:audioQueue buffer:buffer];
    }
}

@end