// // AudioStreamer.m // StreamingAudioPlayer // // Created by Matt Gallagher on 27/09/08. // Copyright 2008 Matt Gallagher. All rights reserved. // // Permission is given to use this source code file, free of charge, in any // project, commercial or otherwise, entirely at your risk, with the condition // that any redistribution (in part or whole) of source code must retain // this copyright and permission notice. Attribution in compiled projects is // appreciated but not required. // #import "AudioStreamer.h" #ifdef TARGET_OS_IPHONE #import <CFNetwork/CFNetwork.h> #endif #define BitRateEstimationMaxPackets 5000 #define BitRateEstimationMinPackets 50 NSString * const ASStatusChangedNotification = @"ASStatusChangedNotification"; NSString * const AS_NO_ERROR_STRING = @"No error."; NSString * const AS_FILE_STREAM_GET_PROPERTY_FAILED_STRING = @"File stream get property failed."; NSString * const AS_FILE_STREAM_SEEK_FAILED_STRING = @"File stream seek failed."; NSString * const AS_FILE_STREAM_PARSE_BYTES_FAILED_STRING = @"Parse bytes failed."; NSString * const AS_FILE_STREAM_OPEN_FAILED_STRING = @"Open audio file stream failed."; NSString * const AS_FILE_STREAM_CLOSE_FAILED_STRING = @"Close audio file stream failed."; NSString * const AS_AUDIO_QUEUE_CREATION_FAILED_STRING = @"Audio queue creation failed."; NSString * const AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED_STRING = @"Audio buffer allocation failed."; NSString * const AS_AUDIO_QUEUE_ENQUEUE_FAILED_STRING = @"Queueing of audio buffer failed."; NSString * const AS_AUDIO_QUEUE_ADD_LISTENER_FAILED_STRING = @"Audio queue add listener failed."; NSString * const AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED_STRING = @"Audio queue remove listener failed."; NSString * const AS_AUDIO_QUEUE_START_FAILED_STRING = @"Audio queue start failed."; NSString * const AS_AUDIO_QUEUE_BUFFER_MISMATCH_STRING = @"Audio queue buffers don't match."; NSString * const AS_AUDIO_QUEUE_DISPOSE_FAILED_STRING = @"Audio queue dispose failed."; NSString * const AS_AUDIO_QUEUE_PAUSE_FAILED_STRING = @"Audio queue pause failed."; NSString * const AS_AUDIO_QUEUE_STOP_FAILED_STRING = @"Audio queue stop failed."; NSString * const AS_AUDIO_DATA_NOT_FOUND_STRING = @"No audio data found."; NSString * const AS_AUDIO_QUEUE_FLUSH_FAILED_STRING = @"Audio queue flush failed."; NSString * const AS_GET_AUDIO_TIME_FAILED_STRING = @"Audio queue get current time failed."; NSString * const AS_AUDIO_STREAMER_FAILED_STRING = @"Audio playback failed"; NSString * const AS_NETWORK_CONNECTION_FAILED_STRING = @"Network connection failed"; NSString * const AS_AUDIO_BUFFER_TOO_SMALL_STRING = @"Audio packets are larger than kAQDefaultBufSize."; @interface AudioStreamer () @property (readwrite) AudioStreamerState state; - (void)handlePropertyChangeForFileStream:(AudioFileStreamID)inAudioFileStream fileStreamPropertyID:(AudioFileStreamPropertyID)inPropertyID ioFlags:(UInt32 *)ioFlags; - (void)handleAudioPackets:(const void *)inInputData numberBytes:(UInt32)inNumberBytes numberPackets:(UInt32)inNumberPackets packetDescriptions:(AudioStreamPacketDescription *)inPacketDescriptions; - (void)handleBufferCompleteForQueue:(AudioQueueRef)inAQ buffer:(AudioQueueBufferRef)inBuffer; - (void)handlePropertyChangeForQueue:(AudioQueueRef)inAQ propertyID:(AudioQueuePropertyID)inID; #ifdef TARGET_OS_IPHONE - (void)handleInterruptionChangeToState:(AudioQueuePropertyID)inInterruptionState; #endif - (void)internalSeekToTime:(double)newSeekTime; - (void)enqueueBuffer; - (void)handleReadFromStream:(CFReadStreamRef)aStream eventType:(CFStreamEventType)eventType; @end #pragma mark Audio Callback Function Prototypes void MyAudioQueueOutputCallback(void* inClientData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer); void MyAudioQueueIsRunningCallback(void *inUserData, AudioQueueRef inAQ, AudioQueuePropertyID inID); void MyPropertyListenerProc( void * inClientData, AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 * ioFlags); void MyPacketsProc( void * inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void * inInputData, AudioStreamPacketDescription *inPacketDescriptions); OSStatus MyEnqueueBuffer(AudioStreamer* myData); #ifdef TARGET_OS_IPHONE void MyAudioSessionInterruptionListener(void *inClientData, UInt32 inInterruptionState); #endif #pragma mark Audio Callback Function Implementations // // MyPropertyListenerProc // // Receives notification when the AudioFileStream has audio packets to be // played. In response, this function creates the AudioQueue, getting it // ready to begin playback (playback won't begin until audio packets are // sent to the queue in MyEnqueueBuffer). // // This function is adapted from Apple's example in AudioFileStreamExample with // kAudioQueueProperty_IsRunning listening added. // void MyPropertyListenerProc( void * inClientData, AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 * ioFlags) { // this is called by audio file stream when it finds property values AudioStreamer* streamer = (AudioStreamer *)inClientData; [streamer handlePropertyChangeForFileStream:inAudioFileStream fileStreamPropertyID:inPropertyID ioFlags:ioFlags]; } // // MyPacketsProc // // When the AudioStream has packets to be played, this function gets an // idle audio buffer and copies the audio packets into it. The calls to // MyEnqueueBuffer won't return until there are buffers available (or the // playback has been stopped). // // This function is adapted from Apple's example in AudioFileStreamExample with // CBR functionality added. // void MyPacketsProc( void * inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void * inInputData, AudioStreamPacketDescription *inPacketDescriptions) { // this is called by audio file stream when it finds packets of audio AudioStreamer* streamer = (AudioStreamer *)inClientData; [streamer handleAudioPackets:inInputData numberBytes:inNumberBytes numberPackets:inNumberPackets packetDescriptions:inPacketDescriptions]; } // // MyAudioQueueOutputCallback // // Called from the AudioQueue when playback of specific buffers completes. This // function signals from the AudioQueue thread to the AudioStream thread that // the buffer is idle and available for copying data. // // This function is unchanged from Apple's example in AudioFileStreamExample. // void MyAudioQueueOutputCallback( void* inClientData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) { // this is called by the audio queue when it has finished decoding our data. // The buffer is now free to be reused. AudioStreamer* streamer = (AudioStreamer*)inClientData; [streamer handleBufferCompleteForQueue:inAQ buffer:inBuffer]; } // // MyAudioQueueIsRunningCallback // // Called from the AudioQueue when playback is started or stopped. This // information is used to toggle the observable "isPlaying" property and // set the "finished" flag. // void MyAudioQueueIsRunningCallback(void *inUserData, AudioQueueRef inAQ, AudioQueuePropertyID inID) { AudioStreamer* streamer = (AudioStreamer *)inUserData; [streamer handlePropertyChangeForQueue:inAQ propertyID:inID]; } #ifdef TARGET_OS_IPHONE // // MyAudioSessionInterruptionListener // // Invoked if the audio session is interrupted (like when the phone rings) // void MyAudioSessionInterruptionListener(void *inClientData, UInt32 inInterruptionState) { AudioStreamer* streamer = (AudioStreamer *)inClientData; [streamer handleInterruptionChangeToState:inInterruptionState]; } #endif #pragma mark CFReadStream Callback Function Implementations // // ReadStreamCallBack // // This is the callback for the CFReadStream from the network connection. This // is where all network data is passed to the AudioFileStream. // // Invoked when an error occurs, the stream ends or we have data to read. // void ASReadStreamCallBack ( CFReadStreamRef aStream, CFStreamEventType eventType, void* inClientInfo ) { AudioStreamer* streamer = (AudioStreamer *)inClientInfo; [streamer handleReadFromStream:aStream eventType:eventType]; } @implementation AudioStreamer @synthesize errorCode; @synthesize state; @synthesize bitRate; @synthesize httpHeaders; // // initWithURL // // Init method for the object. // - (id)initWithURL:(NSURL *)aURL { self = [super init]; if (self != nil) { url = [aURL retain]; } return self; } // // dealloc // // Releases instance memory. // - (void)dealloc { [self stop]; [url release]; [super dealloc]; } // // isFinishing // // returns YES if the audio has reached a stopping condition. // - (BOOL)isFinishing { @synchronized (self) { if ((errorCode != AS_NO_ERROR && state != AS_INITIALIZED) || ((state == AS_STOPPING || state == AS_STOPPED) && stopReason != AS_STOPPING_TEMPORARILY)) { return YES; } } return NO; } // // runLoopShouldExit // // returns YES if the run loop should exit. // - (BOOL)runLoopShouldExit { @synchronized(self) { if (errorCode != AS_NO_ERROR || (state == AS_STOPPED && stopReason != AS_STOPPING_TEMPORARILY)) { return YES; } } return NO; } // // stringForErrorCode: // // Converts an error code to a string that can be localized or presented // to the user. // // Parameters: // anErrorCode - the error code to convert // // returns the string representation of the error code // + (NSString *)stringForErrorCode:(AudioStreamerErrorCode)anErrorCode { switch (anErrorCode) { case AS_NO_ERROR: return AS_NO_ERROR_STRING; case AS_FILE_STREAM_GET_PROPERTY_FAILED: return AS_FILE_STREAM_GET_PROPERTY_FAILED_STRING; case AS_FILE_STREAM_SEEK_FAILED: return AS_FILE_STREAM_SEEK_FAILED_STRING; case AS_FILE_STREAM_PARSE_BYTES_FAILED: return AS_FILE_STREAM_PARSE_BYTES_FAILED_STRING; case AS_AUDIO_QUEUE_CREATION_FAILED: return AS_AUDIO_QUEUE_CREATION_FAILED_STRING; case AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED: return AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED_STRING; case AS_AUDIO_QUEUE_ENQUEUE_FAILED: return AS_AUDIO_QUEUE_ENQUEUE_FAILED_STRING; case AS_AUDIO_QUEUE_ADD_LISTENER_FAILED: return AS_AUDIO_QUEUE_ADD_LISTENER_FAILED_STRING; case AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED: return AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED_STRING; case AS_AUDIO_QUEUE_START_FAILED: return AS_AUDIO_QUEUE_START_FAILED_STRING; case AS_AUDIO_QUEUE_BUFFER_MISMATCH: return AS_AUDIO_QUEUE_BUFFER_MISMATCH_STRING; case AS_FILE_STREAM_OPEN_FAILED: return AS_FILE_STREAM_OPEN_FAILED_STRING; case AS_FILE_STREAM_CLOSE_FAILED: return AS_FILE_STREAM_CLOSE_FAILED_STRING; case AS_AUDIO_QUEUE_DISPOSE_FAILED: return AS_AUDIO_QUEUE_DISPOSE_FAILED_STRING; case AS_AUDIO_QUEUE_PAUSE_FAILED: return AS_AUDIO_QUEUE_DISPOSE_FAILED_STRING; case AS_AUDIO_QUEUE_FLUSH_FAILED: return AS_AUDIO_QUEUE_FLUSH_FAILED_STRING; case AS_AUDIO_DATA_NOT_FOUND: return AS_AUDIO_DATA_NOT_FOUND_STRING; case AS_GET_AUDIO_TIME_FAILED: return AS_GET_AUDIO_TIME_FAILED_STRING; case AS_NETWORK_CONNECTION_FAILED: return AS_NETWORK_CONNECTION_FAILED_STRING; case AS_AUDIO_QUEUE_STOP_FAILED: return AS_AUDIO_QUEUE_STOP_FAILED_STRING; case AS_AUDIO_STREAMER_FAILED: return AS_AUDIO_STREAMER_FAILED_STRING; case AS_AUDIO_BUFFER_TOO_SMALL: return AS_AUDIO_BUFFER_TOO_SMALL_STRING; default: return AS_AUDIO_STREAMER_FAILED_STRING; } return AS_AUDIO_STREAMER_FAILED_STRING; } // // presentAlertWithTitle:message: // // Common code for presenting error dialogs // // Parameters: // title - title for the dialog // message - main test for the dialog // - (void)presentAlertWithTitle:(NSString*)title message:(NSString*)message { #ifdef TARGET_OS_IPHONE UIAlertView *alert = [ [[UIAlertView alloc] initWithTitle:title message:message delegate:self cancelButtonTitle:NSLocalizedString(@"OK", @"") otherButtonTitles: nil] autorelease]; [alert performSelector:@selector(show) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO]; #else NSAlert *alert = [NSAlert alertWithMessageText:title defaultButton:NSLocalizedString(@"OK", @"") alternateButton:nil otherButton:nil informativeTextWithFormat:message]; [alert performSelector:@selector(runModal) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO]; #endif } // // failWithErrorCode: // // Sets the playback state to failed and logs the error. // // Parameters: // anErrorCode - the error condition // - (void)failWithErrorCode:(AudioStreamerErrorCode)anErrorCode { @synchronized(self) { if (errorCode != AS_NO_ERROR) { // Only set the error once. return; } errorCode = anErrorCode; if (err) { char *errChars = (char *)&err; NSLog(@"%@ err: %c%c%c%c %d\n", [AudioStreamer stringForErrorCode:anErrorCode], errChars[3], errChars[2], errChars[1], errChars[0], (int)err); } else { NSLog(@"%@", [AudioStreamer stringForErrorCode:anErrorCode]); } if (state == AS_PLAYING || state == AS_PAUSED || state == AS_BUFFERING) { self.state = AS_STOPPING; stopReason = AS_STOPPING_ERROR; AudioQueueStop(audioQueue, true); } [self presentAlertWithTitle:NSLocalizedStringFromTable(@"File Error", @"Errors", nil) message:NSLocalizedStringFromTable(@"Unable to configure network read stream.", @"Errors", nil)]; } } // // mainThreadStateNotification // // Method invoked on main thread to send notifications to the main thread's // notification center. // - (void)mainThreadStateNotification { NSNotification *notification = [NSNotification notificationWithName:ASStatusChangedNotification object:self]; [[NSNotificationCenter defaultCenter] postNotification:notification]; } // // setState: // // Sets the state and sends a notification that the state has changed. // // This method // // Parameters: // anErrorCode - the error condition // - (void)setState:(AudioStreamerState)aStatus { @synchronized(self) { if (state != aStatus) { state = aStatus; if ([[NSThread currentThread] isEqual:[NSThread mainThread]]) { [self mainThreadStateNotification]; } else { [self performSelectorOnMainThread:@selector(mainThreadStateNotification) withObject:nil waitUntilDone:NO]; } } } } // // isPlaying // // returns YES if the audio currently playing. // - (BOOL)isPlaying { if (state == AS_PLAYING) { return YES; } return NO; } // // isPaused // // returns YES if the audio currently playing. // - (BOOL)isPaused { if (state == AS_PAUSED) { return YES; } return NO; } // // isWaiting // // returns YES if the AudioStreamer is waiting for a state transition of some // kind. // - (BOOL)isWaiting { @synchronized(self) { if ([self isFinishing] || state == AS_STARTING_FILE_THREAD|| state == AS_WAITING_FOR_DATA || state == AS_WAITING_FOR_QUEUE_TO_START || state == AS_BUFFERING) { return YES; } } return NO; } // // isIdle // // returns YES if the AudioStream is in the AS_INITIALIZED state (i.e. // isn't doing anything). // - (BOOL)isIdle { if (state == AS_INITIALIZED) { return YES; } return NO; } // // hintForFileExtension: // // Generates a first guess for the file type based on the file's extension // // Parameters: // fileExtension - the file extension // // returns a file type hint that can be passed to the AudioFileStream // + (AudioFileTypeID)hintForFileExtension:(NSString *)fileExtension { AudioFileTypeID fileTypeHint = kAudioFileMP3Type; if ([fileExtension isEqual:@"mp3"]) { fileTypeHint = kAudioFileMP3Type; } else if ([fileExtension isEqual:@"wav"]) { fileTypeHint = kAudioFileWAVEType; } else if ([fileExtension isEqual:@"aifc"]) { fileTypeHint = kAudioFileAIFCType; } else if ([fileExtension isEqual:@"aiff"]) { fileTypeHint = kAudioFileAIFFType; } else if ([fileExtension isEqual:@"m4a"]) { fileTypeHint = kAudioFileM4AType; } else if ([fileExtension isEqual:@"mp4"]) { fileTypeHint = kAudioFileMPEG4Type; } else if ([fileExtension isEqual:@"caf"]) { fileTypeHint = kAudioFileCAFType; } else if ([fileExtension isEqual:@"aac"]) { fileTypeHint = kAudioFileAAC_ADTSType; } return fileTypeHint; } // // openReadStream // // Open the audioFileStream to parse data and the fileHandle as the data // source. // - (BOOL)openReadStream { @synchronized(self) { NSAssert([[NSThread currentThread] isEqual:internalThread], @"File stream download must be started on the internalThread"); NSAssert(stream == nil, @"Download stream already initialized"); // // Create the HTTP GET request // CFHTTPMessageRef message= CFHTTPMessageCreateRequest(NULL, (CFStringRef)@"GET", (CFURLRef)url, kCFHTTPVersion1_1); // // If we are creating this request to seek to a location, set the // requested byte range in the headers. // if (fileLength > 0 && seekByteOffset > 0) { CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Range"), (CFStringRef)[NSString stringWithFormat:@"bytes=%ld-%ld", seekByteOffset, fileLength]); discontinuous = YES; } // // Create the read stream that will receive data from the HTTP request // stream = CFReadStreamCreateForHTTPRequest(NULL, message); CFRelease(message); // // Enable stream redirection // if (CFReadStreamSetProperty( stream, kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanTrue) == false) { [self presentAlertWithTitle:NSLocalizedStringFromTable(@"File Error", @"Errors", nil) message:NSLocalizedStringFromTable(@"Unable to configure network read stream.", @"Errors", nil)]; return NO; } // // Handle SSL connections // if( [[url absoluteString] rangeOfString:@"https"].location != NSNotFound ) { NSDictionary *sslSettings = [NSDictionary dictionaryWithObjectsAndKeys: (NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL, kCFStreamSSLLevel, [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates, [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredRoots, [NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot, [NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain, [NSNull null], kCFStreamSSLPeerName, nil]; CFReadStreamSetProperty(stream, kCFStreamPropertySSLSettings, sslSettings); } // // We're now ready to receive data // self.state = AS_WAITING_FOR_DATA; // // Open the stream // if (!CFReadStreamOpen(stream)) { CFRelease(stream); [self presentAlertWithTitle:NSLocalizedStringFromTable(@"File Error", @"Errors", nil) message:NSLocalizedStringFromTable(@"Unable to configure network read stream.", @"Errors", nil)]; return NO; } // // Set our callback function to receive the data // CFStreamClientContext context = {0, self, NULL, NULL, NULL}; CFReadStreamSetClient( stream, kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, ASReadStreamCallBack, &context); CFReadStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); } return YES; } // // startInternal // // This is the start method for the AudioStream thread. This thread is created // because it will be blocked when there are no audio buffers idle (and ready // to receive audio data). // // Activity in this thread: // - Creation and cleanup of all AudioFileStream and AudioQueue objects // - Receives data from the CFReadStream // - AudioFileStream processing // - Copying of data from AudioFileStream into audio buffers // - Stopping of the thread because of end-of-file // - Stopping due to error or failure // // Activity *not* in this thread: // - AudioQueue playback and notifications (happens in AudioQueue thread) // - Actual download of NSURLConnection data (NSURLConnection's thread) // - Creation of the AudioStreamer (other, likely "main" thread) // - Invocation of -start method (other, likely "main" thread) // - User/manual invocation of -stop (other, likely "main" thread) // // This method contains bits of the "main" function from Apple's example in // AudioFileStreamExample. // - (void)startInternal { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; @synchronized(self) { if (state != AS_STARTING_FILE_THREAD) { if (state != AS_STOPPING && state != AS_STOPPED) { NSLog(@"### Not starting audio thread. State code is: %ld", state); } self.state = AS_INITIALIZED; [pool release]; return; } #ifdef TARGET_OS_IPHONE // // Set the audio session category so that we continue to play if the // iPhone/iPod auto-locks. // AudioSessionInitialize ( NULL, // 'NULL' to use the default (main) run loop NULL, // 'NULL' to use the default run loop mode MyAudioSessionInterruptionListener, // a reference to your interruption callback self // data to pass to your interruption listener callback ); UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback; AudioSessionSetProperty ( kAudioSessionProperty_AudioCategory, sizeof (sessionCategory), &sessionCategory ); AudioSessionSetActive(true); #endif // initialize a mutex and condition so that we can block on buffers in use. pthread_mutex_init(&queueBuffersMutex, NULL); pthread_cond_init(&queueBufferReadyCondition, NULL); if (![self openReadStream]) { goto cleanup; } } // // Process the run loop until playback is finished or failed. // BOOL isRunning = YES; do { isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]]; @synchronized(self) { if (seekWasRequested) { [self internalSeekToTime:requestedSeekTime]; seekWasRequested = NO; } } // // If there are no queued buffers, we need to check here since the // handleBufferCompleteForQueue:buffer: should not change the state // (may not enter the synchronized section). // if (buffersUsed == 0 && self.state == AS_PLAYING) { err = AudioQueuePause(audioQueue); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_PAUSE_FAILED]; return; } self.state = AS_BUFFERING; } } while (isRunning && ![self runLoopShouldExit]); cleanup: @synchronized(self) { // // Cleanup the read stream if it is still open // if (stream) { CFReadStreamClose(stream); CFRelease(stream); stream = nil; } // // Close the audio file strea, // if (audioFileStream) { err = AudioFileStreamClose(audioFileStream); audioFileStream = nil; if (err) { [self failWithErrorCode:AS_FILE_STREAM_CLOSE_FAILED]; } } // // Dispose of the Audio Queue // if (audioQueue) { err = AudioQueueDispose(audioQueue, true); audioQueue = nil; if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_DISPOSE_FAILED]; } } pthread_mutex_destroy(&queueBuffersMutex); pthread_cond_destroy(&queueBufferReadyCondition); #ifdef TARGET_OS_IPHONE AudioSessionSetActive(false); #endif [httpHeaders release]; httpHeaders = nil; bytesFilled = 0; packetsFilled = 0; seekByteOffset = 0; packetBufferSize = 0; self.state = AS_INITIALIZED; [internalThread release]; internalThread = nil; } [pool release]; } // // start // // Calls startInternal in a new thread. // - (void)start { @synchronized (self) { if (state == AS_PAUSED) { [self pause]; } else if (state == AS_INITIALIZED) { NSAssert([[NSThread currentThread] isEqual:[NSThread mainThread]], @"Playback can only be started from the main thread."); notificationCenter = [[NSNotificationCenter defaultCenter] retain]; self.state = AS_STARTING_FILE_THREAD; internalThread = [[NSThread alloc] initWithTarget:self selector:@selector(startInternal) object:nil]; [internalThread start]; } } } // internalSeekToTime: // // Called from our internal runloop to reopen the stream at a seeked location // - (void)internalSeekToTime:(double)newSeekTime { if ([self calculatedBitRate] == 0.0 || fileLength <= 0) { return; } // // Calculate the byte offset for seeking // seekByteOffset = dataOffset + (newSeekTime / self.duration) * (fileLength - dataOffset); // // Attempt to leave 1 useful packet at the end of the file (although in // reality, this may still seek too far if the file has a long trailer). // if (seekByteOffset > fileLength - 2 * packetBufferSize) { seekByteOffset = fileLength - 2 * packetBufferSize; } // // Store the old time from the audio queue and the time that we're seeking // to so that we'll know the correct time progress after seeking. // seekTime = newSeekTime; // // Attempt to align the seek with a packet boundary // double calculatedBitRate = [self calculatedBitRate]; if (packetDuration > 0 && calculatedBitRate > 0) { UInt32 ioFlags = 0; SInt64 packetAlignedByteOffset; SInt64 seekPacket = floor(newSeekTime / packetDuration); err = AudioFileStreamSeek(audioFileStream, seekPacket, &packetAlignedByteOffset, &ioFlags); if (!err && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated)) { seekTime -= ((seekByteOffset - dataOffset) - packetAlignedByteOffset) * 8.0 / calculatedBitRate; seekByteOffset = packetAlignedByteOffset + dataOffset; } } // // Close the current read straem // if (stream) { CFReadStreamClose(stream); CFRelease(stream); stream = nil; } // // Stop the audio queue // self.state = AS_STOPPING; stopReason = AS_STOPPING_TEMPORARILY; err = AudioQueueStop(audioQueue, true); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_STOP_FAILED]; return; } // // Re-open the file stream. It will request a byte-range starting at // seekByteOffset. // [self openReadStream]; } // // seekToTime: // // Attempts to seek to the new time. Will be ignored if the bitrate or fileLength // are unknown. // // Parameters: // newTime - the time to seek to // - (void)seekToTime:(double)newSeekTime { @synchronized(self) { seekWasRequested = YES; requestedSeekTime = newSeekTime; } } // // progress // // returns the current playback progress. Will return zero if sampleRate has // not yet been detected. // - (double)progress { @synchronized(self) { if (sampleRate > 0 && ![self isFinishing]) { if (state != AS_PLAYING && state != AS_PAUSED && state != AS_BUFFERING) { return lastProgress; } AudioTimeStamp queueTime; Boolean discontinuity; err = AudioQueueGetCurrentTime(audioQueue, NULL, &queueTime, &discontinuity); const OSStatus AudioQueueStopped = 0x73746F70; // 0x73746F70 is 'stop' if (err == AudioQueueStopped) { return lastProgress; } else if (err) { [self failWithErrorCode:AS_GET_AUDIO_TIME_FAILED]; } double progress = seekTime + queueTime.mSampleTime / sampleRate; if (progress < 0.0) { progress = 0.0; } lastProgress = progress; return progress; } } return lastProgress; } // // calculatedBitRate // // returns the bit rate, if known. Uses packet duration times running bits per // packet if available, otherwise it returns the nominal bitrate. Will return // zero if no useful option available. // - (double)calculatedBitRate { if (packetDuration && processedPacketsCount > BitRateEstimationMinPackets) { double averagePacketByteSize = processedPacketsSizeTotal / processedPacketsCount; return 8.0 * averagePacketByteSize / packetDuration; } if (bitRate) { return (double)bitRate; } return 0; } // // duration // // Calculates the duration of available audio from the bitRate and fileLength. // // returns the calculated duration in seconds. // - (double)duration { double calculatedBitRate = [self calculatedBitRate]; if (calculatedBitRate == 0 || fileLength == 0) { return 0.0; } return (fileLength - dataOffset) / (calculatedBitRate * 0.125); } // // pause // // A togglable pause function. // - (void)pause { @synchronized(self) { if (state == AS_PLAYING) { err = AudioQueuePause(audioQueue); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_PAUSE_FAILED]; return; } self.state = AS_PAUSED; } else if (state == AS_PAUSED) { err = AudioQueueStart(audioQueue, NULL); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_START_FAILED]; return; } self.state = AS_PLAYING; } } } // // stop // // This method can be called to stop downloading/playback before it completes. // It is automatically called when an error occurs. // // If playback has not started before this method is called, it will toggle the // "isPlaying" property so that it is guaranteed to transition to true and // back to false // - (void)stop { @synchronized(self) { if (audioQueue && (state == AS_PLAYING || state == AS_PAUSED || state == AS_BUFFERING || state == AS_WAITING_FOR_QUEUE_TO_START)) { self.state = AS_STOPPING; stopReason = AS_STOPPING_USER_ACTION; err = AudioQueueStop(audioQueue, true); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_STOP_FAILED]; return; } } else if (state != AS_INITIALIZED) { self.state = AS_STOPPED; stopReason = AS_STOPPING_USER_ACTION; } seekWasRequested = NO; } while (state != AS_INITIALIZED) { [NSThread sleepForTimeInterval:0.1]; } } // // handleReadFromStream:eventType: // // Reads data from the network file stream into the AudioFileStream // // Parameters: // aStream - the network file stream // eventType - the event which triggered this method // - (void)handleReadFromStream:(CFReadStreamRef)aStream eventType:(CFStreamEventType)eventType { if (aStream != stream) { // // Ignore messages from old streams // return; } if (eventType == kCFStreamEventErrorOccurred) { [self failWithErrorCode:AS_AUDIO_DATA_NOT_FOUND]; } else if (eventType == kCFStreamEventEndEncountered) { @synchronized(self) { if ([self isFinishing]) { return; } } // // If there is a partially filled buffer, pass it to the AudioQueue for // processing // if (bytesFilled) { if (self.state == AS_WAITING_FOR_DATA) { // // Force audio data smaller than one whole buffer to play. // self.state = AS_FLUSHING_EOF; } [self enqueueBuffer]; } @synchronized(self) { if (state == AS_WAITING_FOR_DATA) { [self failWithErrorCode:AS_AUDIO_DATA_NOT_FOUND]; } // // We left the synchronized section to enqueue the buffer so we // must check that we are !finished again before touching the // audioQueue // else if (![self isFinishing]) { if (audioQueue) { // // Set the progress at the end of the stream // err = AudioQueueFlush(audioQueue); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_FLUSH_FAILED]; return; } self.state = AS_STOPPING; stopReason = AS_STOPPING_EOF; err = AudioQueueStop(audioQueue, false); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_FLUSH_FAILED]; return; } } else { self.state = AS_STOPPED; stopReason = AS_STOPPING_EOF; } } } } else if (eventType == kCFStreamEventHasBytesAvailable) { if (!httpHeaders) { CFTypeRef message = CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader); httpHeaders = (NSDictionary *)CFHTTPMessageCopyAllHeaderFields((CFHTTPMessageRef)message); CFRelease(message); // // Only read the content length if we seeked to time zero, otherwise // we only have a subset of the total bytes. // if (seekByteOffset == 0) { fileLength = [[httpHeaders objectForKey:@"Content-Length"] integerValue]; } } if (!audioFileStream) { // // Attempt to guess the file type from the URL. Reading the MIME type // from the httpHeaders might be a better approach since lots of // URL's don't have the right extension. // // If you have a fixed file-type, you may want to hardcode this. // AudioFileTypeID fileTypeHint = [AudioStreamer hintForFileExtension:[[url path] pathExtension]]; // create an audio file stream parser err = AudioFileStreamOpen(self, MyPropertyListenerProc, MyPacketsProc, fileTypeHint, &audioFileStream); if (err) { [self failWithErrorCode:AS_FILE_STREAM_OPEN_FAILED]; return; } } UInt8 bytes[kAQDefaultBufSize]; CFIndex length; @synchronized(self) { if ([self isFinishing] || !CFReadStreamHasBytesAvailable(stream)) { return; } // // Read the bytes from the stream // length = CFReadStreamRead(stream, bytes, kAQDefaultBufSize); if (length == -1) { [self failWithErrorCode:AS_AUDIO_DATA_NOT_FOUND]; return; } if (length == 0) { return; } } if (discontinuous) { err = AudioFileStreamParseBytes(audioFileStream, length, bytes, kAudioFileStreamParseFlag_Discontinuity); if (err) { [self failWithErrorCode:AS_FILE_STREAM_PARSE_BYTES_FAILED]; return; } } else { err = AudioFileStreamParseBytes(audioFileStream, length, bytes, 0); if (err) { [self failWithErrorCode:AS_FILE_STREAM_PARSE_BYTES_FAILED]; return; } } } } // // enqueueBuffer // // Called from MyPacketsProc and connectionDidFinishLoading to pass filled audio // bufffers (filled by MyPacketsProc) to the AudioQueue for playback. This // function does not return until a buffer is idle for further filling or // the AudioQueue is stopped. // // This function is adapted from Apple's example in AudioFileStreamExample with // CBR functionality added. // - (void)enqueueBuffer { @synchronized(self) { if ([self isFinishing] || stream == 0) { return; } inuse[fillBufferIndex] = true; // set in use flag buffersUsed++; // enqueue buffer AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; fillBuf->mAudioDataByteSize = bytesFilled; if (packetsFilled) { err = AudioQueueEnqueueBuffer(audioQueue, fillBuf, packetsFilled, packetDescs); } else { err = AudioQueueEnqueueBuffer(audioQueue, fillBuf, 0, NULL); } if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_ENQUEUE_FAILED]; return; } if (state == AS_BUFFERING || state == AS_WAITING_FOR_DATA || state == AS_FLUSHING_EOF || (state == AS_STOPPED && stopReason == AS_STOPPING_TEMPORARILY)) { // // Fill all the buffers before starting. This ensures that the // AudioFileStream stays a small amount ahead of the AudioQueue to // avoid an audio glitch playing streaming files on iPhone SDKs < 3.0 // if (state == AS_FLUSHING_EOF || buffersUsed == kNumAQBufs - 1) { if (self.state == AS_BUFFERING) { err = AudioQueueStart(audioQueue, NULL); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_START_FAILED]; return; } self.state = AS_PLAYING; } else { self.state = AS_WAITING_FOR_QUEUE_TO_START; err = AudioQueueStart(audioQueue, NULL); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_START_FAILED]; return; } } } } // go to next buffer if (++fillBufferIndex >= kNumAQBufs) fillBufferIndex = 0; bytesFilled = 0; // reset bytes filled packetsFilled = 0; // reset packets filled } // wait until next buffer is not in use pthread_mutex_lock(&queueBuffersMutex); while (inuse[fillBufferIndex]) { pthread_cond_wait(&queueBufferReadyCondition, &queueBuffersMutex); } pthread_mutex_unlock(&queueBuffersMutex); } // // createQueue // // Method to create the AudioQueue from the parameters gathered by the // AudioFileStream. // // Creation is deferred to the handling of the first audio packet (although // it could be handled any time after kAudioFileStreamProperty_ReadyToProducePackets // is true). // - (void)createQueue { sampleRate = asbd.mSampleRate; packetDuration = asbd.mFramesPerPacket / sampleRate; // create the audio queue err = AudioQueueNewOutput(&asbd, MyAudioQueueOutputCallback, self, NULL, NULL, 0, &audioQueue); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_CREATION_FAILED]; return; } // start the queue if it has not been started already // listen to the "isRunning" property err = AudioQueueAddPropertyListener(audioQueue, kAudioQueueProperty_IsRunning, MyAudioQueueIsRunningCallback, self); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_ADD_LISTENER_FAILED]; return; } // get the packet size if it is available UInt32 sizeOfUInt32 = sizeof(UInt32); err = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_PacketSizeUpperBound, &sizeOfUInt32, &packetBufferSize); if (err || packetBufferSize == 0) { err = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_MaximumPacketSize, &sizeOfUInt32, &packetBufferSize); if (err || packetBufferSize == 0) { // No packet size available, just use the default packetBufferSize = kAQDefaultBufSize; } } // allocate audio queue buffers for (unsigned int i = 0; i < kNumAQBufs; ++i) { err = AudioQueueAllocateBuffer(audioQueue, packetBufferSize, &audioQueueBuffer[i]); if (err) { [self failWithErrorCode:AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED]; return; } } // get the cookie size UInt32 cookieSize; Boolean writable; OSStatus ignorableError; ignorableError = AudioFileStreamGetPropertyInfo(audioFileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, &writable); if (ignorableError) { return; } // get the cookie data void* cookieData = calloc(1, cookieSize); ignorableError = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, cookieData); if (ignorableError) { return; } // set the cookie on the queue. ignorableError = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_MagicCookie, cookieData, cookieSize); free(cookieData); if (ignorableError) { return; } } // // handlePropertyChangeForFileStream:fileStreamPropertyID:ioFlags: // // Object method which handles implementation of MyPropertyListenerProc // // Parameters: // inAudioFileStream - should be the same as self->audioFileStream // inPropertyID - the property that changed // ioFlags - the ioFlags passed in // - (void)handlePropertyChangeForFileStream:(AudioFileStreamID)inAudioFileStream fileStreamPropertyID:(AudioFileStreamPropertyID)inPropertyID ioFlags:(UInt32 *)ioFlags { @synchronized(self) { if ([self isFinishing]) { return; } if (inPropertyID == kAudioFileStreamProperty_ReadyToProducePackets) { discontinuous = true; } else if (inPropertyID == kAudioFileStreamProperty_DataOffset) { SInt64 offset; UInt32 offsetSize = sizeof(offset); err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &offset); if (err) { [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; return; } dataOffset = offset; if (audioDataByteCount) { fileLength = dataOffset + audioDataByteCount; } } else if (inPropertyID == kAudioFileStreamProperty_AudioDataByteCount) { UInt32 byteCountSize = sizeof(UInt64); err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount); if (err) { [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; return; } fileLength = dataOffset + audioDataByteCount; } else if (inPropertyID == kAudioFileStreamProperty_DataFormat) { if (asbd.mSampleRate == 0) { UInt32 asbdSize = sizeof(asbd); // get the stream format. err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &asbdSize, &asbd); if (err) { [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; return; } } } else if (inPropertyID == kAudioFileStreamProperty_FormatList) { Boolean outWriteable; UInt32 formatListSize; err = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable); if (err) { [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; return; } AudioFormatListItem *formatList = malloc(formatListSize); err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList); if (err) { [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; return; } for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem)) { AudioStreamBasicDescription pasbd = formatList[i].mASBD; if (pasbd.mFormatID == kAudioFormatMPEG4AAC_HE) { // // We've found HE-AAC, remember this to tell the audio queue // when we construct it. // #if !TARGET_IPHONE_SIMULATOR asbd = pasbd; #endif break; } } free(formatList); } else { // NSLog(@"Property is %c%c%c%c", // ((char *)&inPropertyID)[3], // ((char *)&inPropertyID)[2], // ((char *)&inPropertyID)[1], // ((char *)&inPropertyID)[0]); } } } // // handleAudioPackets:numberBytes:numberPackets:packetDescriptions: // // Object method which handles the implementation of MyPacketsProc // // Parameters: // inInputData - the packet data // inNumberBytes - byte size of the data // inNumberPackets - number of packets in the data // inPacketDescriptions - packet descriptions // - (void)handleAudioPackets:(const void *)inInputData numberBytes:(UInt32)inNumberBytes numberPackets:(UInt32)inNumberPackets packetDescriptions:(AudioStreamPacketDescription *)inPacketDescriptions; { @synchronized(self) { if ([self isFinishing]) { return; } if (bitRate == 0) { // // m4a and a few other formats refuse to parse the bitrate so // we need to set an "unparseable" condition here. If you know // the bitrate (parsed it another way) you can set it on the // class if needed. // bitRate = ~0; } // we have successfully read the first packests from the audio stream, so // clear the "discontinuous" flag if (discontinuous) { discontinuous = false; } if (!audioQueue) { [self createQueue]; } } // the following code assumes we're streaming VBR data. for CBR data, the second branch is used. if (inPacketDescriptions) { for (int i = 0; i < inNumberPackets; ++i) { SInt64 packetOffset = inPacketDescriptions[i].mStartOffset; SInt64 packetSize = inPacketDescriptions[i].mDataByteSize; size_t bufSpaceRemaining; if (processedPacketsCount < BitRateEstimationMaxPackets) { processedPacketsSizeTotal += packetSize; processedPacketsCount += 1; } @synchronized(self) { // If the audio was terminated before this point, then // exit. if ([self isFinishing]) { return; } if (packetSize > packetBufferSize) { [self failWithErrorCode:AS_AUDIO_BUFFER_TOO_SMALL]; } bufSpaceRemaining = packetBufferSize - bytesFilled; } // if the space remaining in the buffer is not enough for this packet, then enqueue the buffer. if (bufSpaceRemaining < packetSize) { [self enqueueBuffer]; } @synchronized(self) { // If the audio was terminated while waiting for a buffer, then // exit. if ([self isFinishing]) { return; } // // If there was some kind of issue with enqueueBuffer and we didn't // make space for the new audio data then back out // if (bytesFilled + packetSize >= packetBufferSize) { return; } // copy data to the audio queue buffer AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; memcpy((char*)fillBuf->mAudioData + bytesFilled, (const char*)inInputData + packetOffset, packetSize); // fill out packet description packetDescs[packetsFilled] = inPacketDescriptions[i]; packetDescs[packetsFilled].mStartOffset = bytesFilled; // keep track of bytes filled and packets filled bytesFilled += packetSize; packetsFilled += 1; } // if that was the last free packet description, then enqueue the buffer. size_t packetsDescsRemaining = kAQMaxPacketDescs - packetsFilled; if (packetsDescsRemaining == 0) { [self enqueueBuffer]; } } } else { size_t offset = 0; while (inNumberBytes) { // if the space remaining in the buffer is not enough for this packet, then enqueue the buffer. size_t bufSpaceRemaining = kAQDefaultBufSize - bytesFilled; if (bufSpaceRemaining < inNumberBytes) { [self enqueueBuffer]; } @synchronized(self) { // If the audio was terminated while waiting for a buffer, then // exit. if ([self isFinishing]) { return; } bufSpaceRemaining = kAQDefaultBufSize - bytesFilled; size_t copySize; if (bufSpaceRemaining < inNumberBytes) { copySize = bufSpaceRemaining; } else { copySize = inNumberBytes; } // // If there was some kind of issue with enqueueBuffer and we didn't // make space for the new audio data then back out // if (bytesFilled >= packetBufferSize) { return; } // copy data to the audio queue buffer AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; memcpy((char*)fillBuf->mAudioData + bytesFilled, (const char*)(inInputData + offset), copySize); // keep track of bytes filled and packets filled bytesFilled += copySize; packetsFilled = 0; inNumberBytes -= copySize; offset += copySize; } } } } // // handleBufferCompleteForQueue:buffer: // // Handles the buffer completetion notification from the audio queue // // Parameters: // inAQ - the queue // inBuffer - the buffer // - (void)handleBufferCompleteForQueue:(AudioQueueRef)inAQ buffer:(AudioQueueBufferRef)inBuffer { unsigned int bufIndex = -1; for (unsigned int i = 0; i < kNumAQBufs; ++i) { if (inBuffer == audioQueueBuffer[i]) { bufIndex = i; break; } } if (bufIndex == -1) { [self failWithErrorCode:AS_AUDIO_QUEUE_BUFFER_MISMATCH]; pthread_mutex_lock(&queueBuffersMutex); pthread_cond_signal(&queueBufferReadyCondition); pthread_mutex_unlock(&queueBuffersMutex); return; } // signal waiting thread that the buffer is free. pthread_mutex_lock(&queueBuffersMutex); inuse[bufIndex] = false; buffersUsed--; // // Enable this logging to measure how many buffers are queued at any time. // #if LOG_QUEUED_BUFFERS NSLog(@"Queued buffers: %ld", buffersUsed); #endif pthread_cond_signal(&queueBufferReadyCondition); pthread_mutex_unlock(&queueBuffersMutex); } // // handlePropertyChangeForQueue:propertyID: // // Implementation for MyAudioQueueIsRunningCallback // // Parameters: // inAQ - the audio queue // inID - the property ID // - (void)handlePropertyChangeForQueue:(AudioQueueRef)inAQ propertyID:(AudioQueuePropertyID)inID { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; @synchronized(self) { if (inID == kAudioQueueProperty_IsRunning) { if (state == AS_STOPPING) { self.state = AS_STOPPED; } else if (state == AS_WAITING_FOR_QUEUE_TO_START) { // // Note about this bug avoidance quirk: // // On cleanup of the AudioQueue thread, on rare occasions, there would // be a crash in CFSetContainsValue as a CFRunLoopObserver was getting // removed from the CFRunLoop. // // After lots of testing, it appeared that the audio thread was // attempting to remove CFRunLoop observers from the CFRunLoop after the // thread had already deallocated the run loop. // // By creating an NSRunLoop for the AudioQueue thread, it changes the // thread destruction order and seems to avoid this crash bug -- or // at least I haven't had it since (nasty hard to reproduce error!) // [NSRunLoop currentRunLoop]; self.state = AS_PLAYING; } else { NSLog(@"AudioQueue changed state in unexpected way."); } } } [pool release]; } #ifdef TARGET_OS_IPHONE // // handleInterruptionChangeForQueue:propertyID: // // Implementation for MyAudioQueueInterruptionListener // // Parameters: // inAQ - the audio queue // inID - the property ID // - (void)handleInterruptionChangeToState:(AudioQueuePropertyID)inInterruptionState { if (inInterruptionState == kAudioSessionBeginInterruption) { [self pause]; } else if (inInterruptionState == kAudioSessionEndInterruption) { AudioSessionSetActive( true ); [self pause]; } } #endif @end
audiostreamer.m
最新推荐文章于 2021-02-06 13:35:46 发布