123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905 |
- /*
- * This file is part of the FreeStreamer project,
- * (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi> 穆马帝
- * See the file ''LICENSE'' for using the code.
- *
- * https://github.com/muhku/FreeStreamer
- */
- #import "FSAudioStream.h"
- #import "Reachability.h"
- #include "audio_stream.h"
- #include "stream_configuration.h"
- #include "input_stream.h"
- #import <AVFoundation/AVFoundation.h>
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- #import <AudioToolbox/AudioToolbox.h>
- #import <UIKit/UIKit.h>
- #endif
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- static NSMutableDictionary *fsAudioStreamPrivateActiveSessions = nil;
- #endif
- @interface FSCacheObject : NSObject {
- }
- @property (strong,nonatomic) NSString *path;
- @property (strong,nonatomic) NSString *name;
- @property (strong,nonatomic) NSDictionary *attributes;
- @property (nonatomic,readonly) unsigned long long fileSize;
- @property (nonatomic,readonly) NSDate *modificationDate;
- @end
- @implementation FSCacheObject
- - (unsigned long long)fileSize
- {
- NSNumber *fileSizeNumber = [self.attributes objectForKey:NSFileSize];
- return [fileSizeNumber longLongValue];
- }
- - (NSDate *)modificationDate
- {
- NSDate *date = [self.attributes objectForKey:NSFileModificationDate];
- return date;
- }
- @end
- static NSInteger sortCacheObjects(id co1, id co2, void *keyForSorting)
- {
- FSCacheObject *cached1 = (FSCacheObject *)co1;
- FSCacheObject *cached2 = (FSCacheObject *)co2;
-
- NSDate *d1 = cached1.modificationDate;
- NSDate *d2 = cached2.modificationDate;
-
- return [d1 compare:d2];
- }
- @implementation FSStreamConfiguration
- - (id)init
- {
- self = [super init];
- if (self) {
- NSMutableString *systemVersion = [[NSMutableString alloc] init];
-
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- [systemVersion appendString:@"iOS "];
- [systemVersion appendString:[[UIDevice currentDevice] systemVersion]];
- #else
- [systemVersion appendString:@"OS X"];
- #endif
- self.bufferCount = 64;
- self.bufferSize = 8192;
- self.maxPacketDescs = 512;
- self.httpConnectionBufferSize = 8192;
- self.outputSampleRate = 44100;
- self.outputNumChannels = 2;
- self.bounceInterval = 10;
- self.maxBounceCount = 4; // Max number of bufferings in bounceInterval seconds
- self.startupWatchdogPeriod = 30; // If the stream doesn't start to play in this seconds, the watchdog will fail it
- #ifdef __LP64__
- /* Increase the max in-memory cache to 10 MB with newer 64 bit devices */
- self.maxPrebufferedByteCount = 10000000; // 10 MB
- #else
- self.maxPrebufferedByteCount = 1000000; // 1 MB
- #endif
- self.userAgent = [NSString stringWithFormat:@"FreeStreamer/%@ (%@)", freeStreamerReleaseVersion(), systemVersion];
- self.cacheEnabled = YES;
- self.seekingFromCacheEnabled = YES;
- self.automaticAudioSessionHandlingEnabled = YES;
- self.enableTimeAndPitchConversion = NO;
- self.requireStrictContentTypeChecking = YES;
- self.maxDiskCacheSize = 256000000; // 256 MB
- self.usePrebufferSizeCalculationInSeconds = YES;
- self.usePrebufferSizeCalculationInPackets = NO;
- self.requiredInitialPrebufferedPacketCount = 32;
- self.requiredPrebufferSizeInSeconds = 7;
- // With dynamic calculation, these are actually the maximum sizes, the dynamic
- // calculation may lower the sizes based on the stream bitrate
- self.requiredInitialPrebufferedByteCountForContinuousStream = 256000;
- self.requiredInitialPrebufferedByteCountForNonContinuousStream = 256000;
-
- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
-
- if ([paths count] > 0) {
- self.cacheDirectory = [paths objectAtIndex:0];
- }
-
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
- AVAudioSession *session = [AVAudioSession sharedInstance];
- double sampleRate = session.sampleRate;
- if (sampleRate > 0) {
- self.outputSampleRate = sampleRate;
- }
- NSInteger channels = session.outputNumberOfChannels;
- if (channels > 0) {
- self.outputNumChannels = channels;
- }
- #endif
-
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- /* iOS */
-
- #else
- /* OS X */
-
- self.requiredPrebufferSizeInSeconds = 3;
-
- // No need to be so concervative with the cache sizes
- self.maxPrebufferedByteCount = 16000000; // 16 MB
- #endif
- }
-
- return self;
- }
- @end
- static NSDateFormatter *statisticsDateFormatter = nil;
- @implementation FSStreamStatistics
- - (NSString *)snapshotTimeFormatted
- {
- if (!statisticsDateFormatter) {
- statisticsDateFormatter = [[NSDateFormatter alloc] init];
- [statisticsDateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
- }
- return [statisticsDateFormatter stringFromDate:self.snapshotTime];
- }
- - (NSString *)description
- {
- return [[NSString alloc] initWithFormat:@"%@\t%lu\t%lu\t%lu",
- self.snapshotTimeFormatted,
- (unsigned long)self.audioStreamPacketCount,
- (unsigned long)self.audioQueueUsedBufferCount,
- (unsigned long)self.audioQueuePCMPacketQueueCount];
- }
- @end
- NSString *freeStreamerReleaseVersion()
- {
- NSString *version = [NSString stringWithFormat:@"%i.%i.%i",
- FREESTREAMER_VERSION_MAJOR,
- FREESTREAMER_VERSION_MINOR,
- FREESTREAMER_VERSION_REVISION];
- return version;
- }
- NSString* const FSAudioStreamStateChangeNotification = @"FSAudioStreamStateChangeNotification";
- NSString* const FSAudioStreamNotificationKey_Stream = @"stream";
- NSString* const FSAudioStreamNotificationKey_State = @"state";
- NSString* const FSAudioStreamErrorNotification = @"FSAudioStreamErrorNotification";
- NSString* const FSAudioStreamNotificationKey_Error = @"error";
- NSString* const FSAudioStreamNotificationKey_ErrorDescription = @"errorDescription";
- NSString* const FSAudioStreamMetaDataNotification = @"FSAudioStreamMetaDataNotification";
- NSString* const FSAudioStreamNotificationKey_MetaData = @"metadata";
- class AudioStreamStateObserver : public astreamer::Audio_Stream_Delegate
- {
- public:
- astreamer::Audio_Stream *source;
- FSAudioStreamPrivate *priv;
-
- void audioStreamErrorOccurred(int errorCode, CFStringRef errorDescription);
- void audioStreamStateChanged(astreamer::Audio_Stream::State state);
- void audioStreamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
- void samplesAvailable(AudioBufferList *samples, UInt32 frames, AudioStreamPacketDescription description);
- void bitrateAvailable();
- };
- /*
- * ===============================================================
- * FSAudioStream private implementation
- * ===============================================================
- */
- @interface FSAudioStreamPrivate : NSObject {
- astreamer::Audio_Stream *_audioStream;
- NSURL *_url;
- AudioStreamStateObserver *_observer;
- NSString *_defaultContentType;
- Reachability *_reachability;
- FSSeekByteOffset _lastSeekByteOffset;
- BOOL _wasPaused;
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- UIBackgroundTaskIdentifier _backgroundTask;
- #endif
- }
- @property (nonatomic,assign) NSURL *url;
- @property (nonatomic,assign) BOOL strictContentTypeChecking;
- @property (nonatomic,assign) NSString *defaultContentType;
- @property (readonly) NSString *contentType;
- @property (readonly) NSString *suggestedFileExtension;
- @property (nonatomic, assign) UInt64 defaultContentLength;
- @property (readonly) UInt64 contentLength;
- @property (nonatomic,assign) NSURL *outputFile;
- @property (nonatomic,assign) BOOL wasInterrupted;
- @property (nonatomic,assign) BOOL wasDisconnected;
- @property (nonatomic,assign) BOOL wasContinuousStream;
- @property (nonatomic,assign) BOOL internetConnectionAvailable;
- @property (nonatomic,assign) NSUInteger maxRetryCount;
- @property (nonatomic,assign) NSUInteger retryCount;
- @property (readonly) FSStreamStatistics *statistics;
- @property (readonly) FSLevelMeterState levels;
- @property (readonly) size_t prebufferedByteCount;
- @property (readonly) FSSeekByteOffset currentSeekByteOffset;
- @property (readonly) float bitRate;
- @property (readonly) FSStreamConfiguration *configuration;
- @property (readonly) NSString *formatDescription;
- @property (readonly) BOOL cached;
- @property (copy) void (^onCompletion)();
- @property (copy) void (^onStateChange)(FSAudioStreamState state);
- @property (copy) void (^onMetaDataAvailable)(NSDictionary *metaData);
- @property (copy) void (^onFailure)(FSAudioStreamError error, NSString *errorDescription);
- @property (nonatomic,unsafe_unretained) id<FSPCMAudioStreamDelegate> delegate;
- @property (nonatomic,unsafe_unretained) FSAudioStream *stream;
- - (AudioStreamStateObserver *)streamStateObserver;
- - (void)endBackgroundTask;
- - (void)reachabilityChanged:(NSNotification *)note;
- - (void)interruptionOccurred:(NSNotification *)notification;
- - (void)notifyPlaybackStopped;
- - (void)notifyPlaybackBuffering;
- - (void)notifyPlaybackPlaying;
- - (void)notifyPlaybackPaused;
- - (void)notifyPlaybackSeeking;
- - (void)notifyPlaybackEndOfFile;
- - (void)notifyPlaybackFailed;
- - (void)notifyPlaybackCompletion;
- - (void)notifyPlaybackUnknownState;
- - (void)notifyRetryingStarted;
- - (void)notifyRetryingSucceeded;
- - (void)notifyRetryingFailed;
- - (void)notifyStateChange:(FSAudioStreamState)streamerState;
- - (void)attemptRestart;
- - (void)expungeCache;
- - (void)play;
- - (void)playFromURL:(NSURL*)url;
- - (void)playFromOffset:(FSSeekByteOffset)offset;
- - (void)stop;
- - (BOOL)isPlaying;
- - (void)pause;
- - (void)rewind:(unsigned)seconds;
- - (void)seekToOffset:(float)offset;
- - (float)currentVolume;
- - (unsigned long long)totalCachedObjectsSize;
- - (void)setVolume:(float)volume;
- - (void)setPlayRate:(float)playRate;
- - (astreamer::AS_Playback_Position)playbackPosition;
- - (UInt64)audioDataByteCount;
- - (float)durationInSeconds;
- - (void)bitrateAvailable;
- @end
- @implementation FSAudioStreamPrivate
- -(id)init
- {
- NSAssert([NSThread isMainThread], @"FSAudioStreamPrivate.init needs to be called in the main thread");
-
- if (self = [super init]) {
- _url = nil;
-
- _observer = new AudioStreamStateObserver();
- _observer->priv = self;
-
- _audioStream = new astreamer::Audio_Stream();
- _observer->source = _audioStream;
- _audioStream->m_delegate = _observer;
-
- _reachability = nil;
-
- _delegate = nil;
-
- _maxRetryCount = 3;
-
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(reachabilityChanged:)
- name:kReachabilityChangedNotification
- object:nil];
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- _backgroundTask = UIBackgroundTaskInvalid;
-
- @synchronized (self) {
- if (!fsAudioStreamPrivateActiveSessions) {
- fsAudioStreamPrivateActiveSessions = [[NSMutableDictionary alloc] init];
- }
- }
-
- if (self.configuration.automaticAudioSessionHandlingEnabled) {
- [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
- }
- #endif
-
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(interruptionOccurred:)
- name:AVAudioSessionInterruptionNotification
- object:nil];
- #endif
- }
- return self;
- }
- - (void)dealloc
- {
- NSAssert([NSThread isMainThread], @"FSAudioStreamPrivate.dealloc needs to be called in the main thread");
-
- [[NSNotificationCenter defaultCenter] removeObserver:self];
-
- [self stop];
-
- _delegate = nil;
-
- delete _audioStream;
- _audioStream = nil;
- delete _observer;
- _observer = nil;
-
- // Clean up the disk cache.
-
- if (!self.configuration.cacheEnabled) {
- // Don't clean up if cache not enabled
- return;
- }
-
- unsigned long long totalCacheSize = 0;
-
- NSMutableArray *cachedFiles = [[NSMutableArray alloc] init];
-
- for (NSString *file in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.configuration.cacheDirectory error:nil]) {
- if ([file hasPrefix:@"FSCache-"]) {
- FSCacheObject *cacheObj = [[FSCacheObject alloc] init];
- cacheObj.name = file;
- cacheObj.path = [NSString stringWithFormat:@"%@/%@", self.configuration.cacheDirectory, cacheObj.name];
- cacheObj.attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:cacheObj.path error:nil];
-
- totalCacheSize += [cacheObj fileSize];
-
- if (![cacheObj.name hasSuffix:@".metadata"]) {
- [cachedFiles addObject:cacheObj];
- }
- }
- }
-
- // Sort by the modification date.
- // In this way the older content will be removed first from the cache.
- [cachedFiles sortUsingFunction:sortCacheObjects context:NULL];
-
- for (FSCacheObject *cacheObj in cachedFiles) {
- if (totalCacheSize < self.configuration.maxDiskCacheSize) {
- break;
- }
-
- FSCacheObject *cachedMetaData = [[FSCacheObject alloc] init];
- cachedMetaData.name = [NSString stringWithFormat:@"%@.metadata", cacheObj.name];
- cachedMetaData.path = [NSString stringWithFormat:@"%@/%@", self.configuration.cacheDirectory, cachedMetaData.name];
- cachedMetaData.attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:cachedMetaData.path error:nil];
-
- if (![[NSFileManager defaultManager] removeItemAtPath:cachedMetaData.path error:nil]) {
- continue;
- }
- totalCacheSize -= [cachedMetaData fileSize];
-
- if (![[NSFileManager defaultManager] removeItemAtPath:cacheObj.path error:nil]) {
- continue;
- }
- totalCacheSize -= [cacheObj fileSize];
- }
-
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- @synchronized (self) {
- [fsAudioStreamPrivateActiveSessions removeObjectForKey:[NSNumber numberWithUnsignedLong:(unsigned long)self]];
-
- if ([fsAudioStreamPrivateActiveSessions count] == 0) {
- if (self.configuration.automaticAudioSessionHandlingEnabled) {
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
- [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- #else
- [[AVAudioSession sharedInstance] setActive:NO error:nil];
- #endif
- }
- }
- }
- #endif
- }
- - (void)endBackgroundTask
- {
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- if (_backgroundTask != UIBackgroundTaskInvalid) {
- [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask];
- _backgroundTask = UIBackgroundTaskInvalid;
- }
- #endif
- }
- - (AudioStreamStateObserver *)streamStateObserver
- {
- return _observer;
- }
- - (void)setUrl:(NSURL *)url
- {
- if ([self isPlaying]) {
- [self stop];
- }
-
- @synchronized (self) {
- if ([url isEqual:_url]) {
- return;
- }
-
- _url = [url copy];
-
- _audioStream->setUrl((__bridge CFURLRef)_url);
- }
-
- if ([self isPlaying]) {
- [self play];
- }
- }
- - (NSURL*)url
- {
- if (!_url) {
- return nil;
- }
-
- NSURL *copyOfURL = [_url copy];
- return copyOfURL;
- }
- - (void)setStrictContentTypeChecking:(BOOL)strictContentTypeChecking
- {
- _audioStream->setStrictContentTypeChecking(strictContentTypeChecking);
- }
- - (BOOL)strictContentTypeChecking
- {
- return _audioStream->strictContentTypeChecking();
- }
- - (void)playFromURL:(NSURL*)url
- {
- [self setUrl:url];
- [self play];
- }
- - (void)playFromOffset:(FSSeekByteOffset)offset
- {
- _wasPaused = NO;
-
- if (_audioStream->isPreloading()) {
- _audioStream->seekToOffset(offset.position);
- _audioStream->setPreloading(false);
- } else {
- astreamer::Input_Stream_Position position;
- position.start = offset.start;
- position.end = offset.end;
-
- _audioStream->open(&position);
-
- _audioStream->setSeekOffset(offset.position);
- _audioStream->setContentLength(offset.end);
- }
-
- if (!_reachability) {
- _reachability = [Reachability reachabilityForInternetConnection];
-
- [_reachability startNotifier];
- }
- }
- - (void)setDefaultContentType:(NSString *)defaultContentType
- {
- if (defaultContentType) {
- _defaultContentType = [defaultContentType copy];
- _audioStream->setDefaultContentType((__bridge CFStringRef)_defaultContentType);
- } else {
- _audioStream->setDefaultContentType(NULL);
- }
- }
- - (NSString*)defaultContentType
- {
- if (!_defaultContentType) {
- return nil;
- }
-
- NSString *copyOfDefaultContentType = [_defaultContentType copy];
- return copyOfDefaultContentType;
- }
- - (NSString*)contentType
- {
- CFStringRef c = _audioStream->contentType();
- if (c) {
- return CFBridgingRelease(CFStringCreateCopy(kCFAllocatorDefault, c));
- }
- return nil;
- }
- - (NSString*)suggestedFileExtension
- {
- NSString *contentType = [self contentType];
- NSString *suggestedFileExtension = nil;
-
- if ([contentType isEqualToString:@"audio/mpeg"]) {
- suggestedFileExtension = @"mp3";
- } else if ([contentType isEqualToString:@"audio/x-wav"]) {
- suggestedFileExtension = @"wav";
- } else if ([contentType isEqualToString:@"audio/x-aifc"]) {
- suggestedFileExtension = @"aifc";
- } else if ([contentType isEqualToString:@"audio/x-aiff"]) {
- suggestedFileExtension = @"aiff";
- } else if ([contentType isEqualToString:@"audio/x-m4a"]) {
- suggestedFileExtension = @"m4a";
- } else if ([contentType isEqualToString:@"audio/mp4"]) {
- suggestedFileExtension = @"mp4";
- } else if ([contentType isEqualToString:@"audio/x-caf"]) {
- suggestedFileExtension = @"caf";
- }
- else if ([contentType isEqualToString:@"audio/aac"] ||
- [contentType isEqualToString:@"audio/aacp"]) {
- suggestedFileExtension = @"aac";
- }
- return suggestedFileExtension;
- }
- - (UInt64)defaultContentLength
- {
- return _audioStream->defaultContentLength();
- }
- - (UInt64)contentLength
- {
- return _audioStream->contentLength();
- }
- - (NSURL*)outputFile
- {
- CFURLRef url = _audioStream->outputFile();
- if (url) {
- NSURL *u = (__bridge NSURL*)url;
- return [u copy];
- }
- return nil;
- }
- - (void)setOutputFile:(NSURL *)outputFile
- {
- if (!outputFile) {
- _audioStream->setOutputFile(NULL);
- return;
- }
- NSURL *copyOfURL = [outputFile copy];
- _audioStream->setOutputFile((__bridge CFURLRef)copyOfURL);
- }
- - (FSStreamStatistics *)statistics
- {
- FSStreamStatistics *stats = [[FSStreamStatistics alloc] init];
-
- stats.snapshotTime = [[NSDate alloc] init];
- stats.audioStreamPacketCount = _audioStream->playbackDataCount();
-
- return stats;
- }
- - (FSLevelMeterState)levels
- {
- AudioQueueLevelMeterState aqLevels = _audioStream->levels();
-
- FSLevelMeterState l;
-
- l.averagePower = aqLevels.mAveragePower;
- l.peakPower = aqLevels.mPeakPower;
-
- return l;
- }
- - (size_t)prebufferedByteCount
- {
- return _audioStream->cachedDataSize();
- }
- - (FSSeekByteOffset)currentSeekByteOffset
- {
- FSSeekByteOffset offset;
- offset.start = 0;
- offset.end = 0;
- offset.position = 0;
-
- // If continuous
- if (!([self durationInSeconds] > 0)) {
- return offset;
- }
-
- offset.position = _audioStream->playbackPosition().offset;
-
- astreamer::Input_Stream_Position httpStreamPos = _audioStream->streamPositionForOffset(offset.position);
-
- offset.start = httpStreamPos.start;
- offset.end = httpStreamPos.end;
-
- return offset;
- }
- - (float)bitRate
- {
- return _audioStream->bitrate();
- }
- - (FSStreamConfiguration *)configuration
- {
- FSStreamConfiguration *config = [[FSStreamConfiguration alloc] init];
-
- astreamer::Stream_Configuration *c = astreamer::Stream_Configuration::configuration();
-
- config.bufferCount = c->bufferCount;
- config.bufferSize = c->bufferSize;
- config.maxPacketDescs = c->maxPacketDescs;
- config.httpConnectionBufferSize = c->httpConnectionBufferSize;
- config.outputSampleRate = c->outputSampleRate;
- config.outputNumChannels = c->outputNumChannels;
- config.bounceInterval = c->bounceInterval;
- config.maxBounceCount = c->maxBounceCount;
- config.startupWatchdogPeriod = c->startupWatchdogPeriod;
- config.maxPrebufferedByteCount = c->maxPrebufferedByteCount;
- config.usePrebufferSizeCalculationInSeconds = c->usePrebufferSizeCalculationInSeconds;
- config.usePrebufferSizeCalculationInPackets = c->usePrebufferSizeCalculationInPackets;
- config.requiredInitialPrebufferedByteCountForContinuousStream = c->requiredInitialPrebufferedByteCountForContinuousStream;
- config.requiredInitialPrebufferedByteCountForNonContinuousStream = c->requiredInitialPrebufferedByteCountForNonContinuousStream;
- config.requiredPrebufferSizeInSeconds = c->requiredPrebufferSizeInSeconds;
- config.requiredInitialPrebufferedPacketCount = c->requiredInitialPrebufferedPacketCount;
- config.cacheEnabled = c->cacheEnabled;
- config.seekingFromCacheEnabled = c->seekingFromCacheEnabled;
- config.automaticAudioSessionHandlingEnabled = c->automaticAudioSessionHandlingEnabled;
- config.enableTimeAndPitchConversion = c->enableTimeAndPitchConversion;
- config.requireStrictContentTypeChecking = c->requireStrictContentTypeChecking;
- config.maxDiskCacheSize = c->maxDiskCacheSize;
-
- if (c->userAgent) {
- // Let the Objective-C side handle the memory for the copy of the original user-agent
- config.userAgent = (__bridge_transfer NSString *)CFStringCreateCopy(kCFAllocatorDefault, c->userAgent);
- }
-
- if (c->cacheDirectory) {
- config.cacheDirectory = (__bridge_transfer NSString *)CFStringCreateCopy(kCFAllocatorDefault, c->cacheDirectory);
- }
-
- if (c->predefinedHttpHeaderValues) {
- config.predefinedHttpHeaderValues = (__bridge_transfer NSDictionary *)CFDictionaryCreateCopy(kCFAllocatorDefault, c->predefinedHttpHeaderValues);
- }
- return config;
- }
- - (NSString *)formatDescription
- {
- return CFBridgingRelease(_audioStream->sourceFormatDescription());
- }
- - (BOOL)cached
- {
- BOOL cachedFileExists = NO;
-
- if (self.url) {
- NSString *cacheIdentifier = (NSString*)CFBridgingRelease(_audioStream->createCacheIdentifierForURL((__bridge CFURLRef)self.url));
-
- NSString *fullPath = [NSString stringWithFormat:@"%@/%@.metadata", self.configuration.cacheDirectory, cacheIdentifier];
-
- cachedFileExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath];
- }
-
- return cachedFileExists;
- }
- - (void)reachabilityChanged:(NSNotification *)note
- {
- NSAssert([NSThread isMainThread], @"FSAudioStreamPrivate.reachabilityChanged needs to be called in the main thread");
-
- Reachability *reach = [note object];
- NetworkStatus netStatus = [reach currentReachabilityStatus];
- self.internetConnectionAvailable = (netStatus == ReachableViaWiFi || netStatus == ReachableViaWWAN);
-
- if ([self isPlaying] && !self.internetConnectionAvailable) {
- self.wasDisconnected = YES;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Error: Internet connection disconnected while playing a stream.");
- #endif
- }
-
- if (self.wasDisconnected && self.internetConnectionAvailable) {
- self.wasDisconnected = NO;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Internet connection available again.");
- #endif
- [self attemptRestart];
- }
- }
- - (void)interruptionOccurred:(NSNotification *)notification
- {
- NSAssert([NSThread isMainThread], @"FSAudioStreamPrivate.interruptionOccurred needs to be called in the main thread");
-
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
- NSNumber *interruptionType = [[notification userInfo] valueForKey:AVAudioSessionInterruptionTypeKey];
- NSNumber *interruptionResume = [[notification userInfo] valueForKey:AVAudioSessionInterruptionOptionKey];
- if ([interruptionType intValue] == AVAudioSessionInterruptionTypeBegan) {
- if ([self isPlaying] && !_wasPaused) {
- self.wasInterrupted = YES;
- // Continuous streams do not have a duration.
- self.wasContinuousStream = !([self durationInSeconds] > 0);
-
- if (self.wasContinuousStream) {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Interruption began. Continuous stream. Stopping the stream.");
- #endif
- [self stop];
- } else {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Interruption began. Non-continuous stream. Stopping the stream and saving the offset.");
- #endif
- _lastSeekByteOffset = [self currentSeekByteOffset];
- [self stop];
- }
- }
- } else if ([interruptionType intValue] == AVAudioSessionInterruptionTypeEnded) {
- if (self.wasInterrupted) {
- self.wasInterrupted = NO;
-
- if ([interruptionResume intValue] == AVAudioSessionInterruptionOptionShouldResume) {
- @synchronized (self) {
- if (self.configuration.automaticAudioSessionHandlingEnabled) {
- [[AVAudioSession sharedInstance] setActive:YES error:nil];
- }
- fsAudioStreamPrivateActiveSessions[[NSNumber numberWithUnsignedLong:(unsigned long)self]] = @"";
- }
-
- if (self.wasContinuousStream) {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Interruption ended. Continuous stream. Starting the playback.");
- #endif
- /*
- * Resume playing.
- */
- [self play];
- } else {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Interruption ended. Continuous stream. Playing from the offset");
- #endif
- /*
- * Resume playing.
- */
- [self playFromOffset:_lastSeekByteOffset];
- }
- } else {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Interruption ended. Continuous stream. Not resuming.");
- #endif
- }
- }
- }
- #endif
- }
- - (void)notifyPlaybackStopped
- {
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- @synchronized (self) {
- [fsAudioStreamPrivateActiveSessions removeObjectForKey:[NSNumber numberWithUnsignedLong:(unsigned long)self]];
-
- if ([fsAudioStreamPrivateActiveSessions count] == 0) {
- if (self.configuration.automaticAudioSessionHandlingEnabled) {
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
- [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- #else
- [[AVAudioSession sharedInstance] setActive:NO error:nil];
- #endif
- }
- }
- }
- #endif
-
- [self notifyStateChange:kFsAudioStreamStopped];
- }
- - (void)notifyPlaybackBuffering
- {
- self.internetConnectionAvailable = YES;
- [self notifyStateChange:kFsAudioStreamBuffering];
- }
- - (void)notifyPlaybackPlaying
- {
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- @synchronized (self) {
- if (self.configuration.automaticAudioSessionHandlingEnabled) {
- [[AVAudioSession sharedInstance] setActive:YES error:nil];
- }
- fsAudioStreamPrivateActiveSessions[[NSNumber numberWithUnsignedLong:(unsigned long)self]] = @"";
- }
- #endif
- if (self.retryCount > 0) {
- [NSTimer scheduledTimerWithTimeInterval:0.1
- target:self
- selector:@selector(notifyRetryingSucceeded)
- userInfo:nil
- repeats:NO];
- }
- self.retryCount = 0;
- [self notifyStateChange:kFsAudioStreamPlaying];
- }
- - (void)notifyPlaybackPaused
- {
- [self notifyStateChange:kFsAudioStreamPaused];
- }
- - (void)notifyPlaybackSeeking
- {
- [self notifyStateChange:kFsAudioStreamSeeking];
- }
- - (void)notifyPlaybackEndOfFile
- {
- [self notifyStateChange:kFSAudioStreamEndOfFile];
- }
- - (void)notifyPlaybackFailed
- {
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- @synchronized (self) {
- [fsAudioStreamPrivateActiveSessions removeObjectForKey:[NSNumber numberWithUnsignedLong:(unsigned long)self]];
-
- if ([fsAudioStreamPrivateActiveSessions count] == 0) {
- if (self.configuration.automaticAudioSessionHandlingEnabled) {
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
- [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- #else
- [[AVAudioSession sharedInstance] setActive:NO error:nil];
- #endif
- }
- }
- }
- #endif
-
- [self notifyStateChange:kFsAudioStreamFailed];
- }
- - (void)notifyPlaybackCompletion
- {
- [self notifyStateChange:kFsAudioStreamPlaybackCompleted];
-
- if (self.onCompletion) {
- self.onCompletion();
- }
- }
- - (void)notifyPlaybackUnknownState
- {
- [self notifyStateChange:kFsAudioStreamUnknownState];
- }
- - (void)notifyRetryingStarted
- {
- [self notifyStateChange:kFsAudioStreamRetryingStarted];
- }
- - (void)notifyRetryingSucceeded
- {
- [self notifyStateChange:kFsAudioStreamRetryingSucceeded];
- }
- - (void)notifyRetryingFailed
- {
- [self notifyStateChange:kFsAudioStreamRetryingFailed];
- }
- - (void)notifyStateChange:(FSAudioStreamState)streamerState
- {
- if (self.onStateChange) {
- self.onStateChange(streamerState);
- }
-
- NSDictionary *userInfo = @{FSAudioStreamNotificationKey_State: [NSNumber numberWithInt:streamerState],
- FSAudioStreamNotificationKey_Stream: [NSValue valueWithPointer:_audioStream]};
- NSNotification *notification = [NSNotification notificationWithName:FSAudioStreamStateChangeNotification object:self.stream userInfo:userInfo];
-
- [[NSNotificationCenter defaultCenter] postNotification:notification];
- }
- - (void)preload
- {
- _audioStream->setPreloading(true);
-
- _audioStream->open();
- }
- - (void)attemptRestart
- {
- if (_audioStream->isPreloading()) {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Stream is preloading. Not attempting a restart");
- #endif
- return;
- }
-
- if (_wasPaused) {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Stream was paused. Not attempting a restart");
- #endif
- return;
- }
-
- if (!self.internetConnectionAvailable) {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Internet connection not available. Not attempting a restart");
- #endif
- return;
- }
-
- if (self.retryCount >= self.maxRetryCount) {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Retry count %lu. Giving up.", (unsigned long)self.retryCount);
- #endif
- [NSTimer scheduledTimerWithTimeInterval:0.1
- target:self
- selector:@selector(notifyRetryingFailed)
- userInfo:nil
- repeats:NO];
- return;
- }
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Attempting restart.");
- #endif
-
- [NSTimer scheduledTimerWithTimeInterval:0.1
- target:self
- selector:@selector(notifyRetryingStarted)
- userInfo:nil
- repeats:NO];
-
- [NSTimer scheduledTimerWithTimeInterval:1
- target:self
- selector:@selector(play)
- userInfo:nil
- repeats:NO];
-
- self.retryCount++;
- }
- - (void)expungeCache
- {
- for (NSString *file in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.configuration.cacheDirectory error:nil]) {
- NSString *fullPath = [NSString stringWithFormat:@"%@/%@", self.configuration.cacheDirectory, file];
-
- if ([file hasPrefix:@"FSCache-"]) {
- if (![[NSFileManager defaultManager] removeItemAtPath:fullPath error:nil]) {
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"Failed expunging %@ from the cache", fullPath);
- #endif
- }
- }
- }
- }
- - (void)play
- {
- _wasPaused = NO;
- if (_audioStream->isPreloading()) {
- _audioStream->startCachedDataPlayback();
-
- return;
- }
-
- #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
- [self endBackgroundTask];
-
- _backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
- [self endBackgroundTask];
- }];
- #endif
-
- _audioStream->open();
- if (!_reachability) {
- _reachability = [Reachability reachabilityForInternetConnection];
-
- [_reachability startNotifier];
- }
- }
- - (void)stop
- {
- _audioStream->close(true);
-
- [self endBackgroundTask];
-
- [_reachability stopNotifier];
- _reachability = nil;
- }
- - (BOOL)isPlaying
- {
- const astreamer::Audio_Stream::State currentState = _audioStream->state();
-
- return (currentState == astreamer::Audio_Stream::PLAYING ||
- currentState == astreamer::Audio_Stream::END_OF_FILE);
- }
- - (void)pause
- {
- _wasPaused = YES;
- _audioStream->pause();
- }
- - (void)rewind:(unsigned int)seconds
- {
- if (([self durationInSeconds] > 0)) {
- // Rewinding only possible for continuous streams
- return;
- }
-
- const float originalVolume = [self currentVolume];
-
- // Set volume to 0 to avoid glitches
- _audioStream->setVolume(0);
-
- _audioStream->rewind(seconds);
-
- __weak FSAudioStreamPrivate *weakSelf = self;
-
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
- FSAudioStreamPrivate *strongSelf = weakSelf;
-
- // Return the original volume back
- strongSelf->_audioStream->setVolume(originalVolume);
- });
- }
- - (void)seekToOffset:(float)offset
- {
- _audioStream->seekToOffset(offset);
- }
- - (float)currentVolume
- {
- return _audioStream->currentVolume();
- }
- - (unsigned long long)totalCachedObjectsSize
- {
- unsigned long long totalCacheSize = 0;
- for (NSString *file in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.configuration.cacheDirectory error:nil]) {
- if ([file hasPrefix:@"FSCache-"]) {
- NSString *fullPath = [NSString stringWithFormat:@"%@/%@", self.configuration.cacheDirectory, file];
- NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];
-
- totalCacheSize += [[attributes objectForKey:NSFileSize] longLongValue];
- }
- }
- return totalCacheSize;
- }
- - (void)setVolume:(float)volume
- {
- _audioStream->setVolume(volume);
- }
- - (void)setPlayRate:(float)playRate
- {
- _audioStream->setPlayRate(playRate);
- }
- - (astreamer::AS_Playback_Position)playbackPosition
- {
- return _audioStream->playbackPosition();
- }
- - (UInt64)audioDataByteCount
- {
- return _audioStream->audioDataByteCount();
- }
- - (float)durationInSeconds
- {
- return _audioStream->durationInSeconds();
- }
- - (void)bitrateAvailable
- {
- if (!self.configuration.usePrebufferSizeCalculationInSeconds) {
- return;
- }
-
- float bitrate = (int)_audioStream->bitrate();
-
- if (!(bitrate > 0)) {
- // No bitrate provided, use the defaults
- return;
- }
-
- const Float64 bufferSizeForSecond = bitrate / 8.0;
-
- int bufferSize = (bufferSizeForSecond * self.configuration.requiredPrebufferSizeInSeconds);
-
- // Check that we still got somewhat sane buffer size
- if (bufferSize < 50000) {
- bufferSize = 50000;
- }
-
- if (!([self durationInSeconds] > 0)) {
- // continuous
- if (bufferSize > self.configuration.requiredInitialPrebufferedByteCountForContinuousStream) {
- bufferSize = self.configuration.requiredInitialPrebufferedByteCountForContinuousStream;
- }
- } else {
- if (bufferSize > self.configuration.requiredInitialPrebufferedByteCountForNonContinuousStream) {
- bufferSize = self.configuration.requiredInitialPrebufferedByteCountForNonContinuousStream;
- }
- }
-
- // Update the configuration
- self.configuration.requiredInitialPrebufferedByteCountForContinuousStream = bufferSize;
- self.configuration.requiredInitialPrebufferedByteCountForNonContinuousStream = bufferSize;
- astreamer::Stream_Configuration *c = astreamer::Stream_Configuration::configuration();
-
- c->requiredInitialPrebufferedByteCountForContinuousStream = bufferSize;
- c->requiredInitialPrebufferedByteCountForNonContinuousStream = bufferSize;
- }
- -(NSString *)description
- {
- return [NSString stringWithFormat:@"[FreeStreamer %@] URL: %@\nbufferCount: %i\nbufferSize: %i\nmaxPacketDescs: %i\nhttpConnectionBufferSize: %i\noutputSampleRate: %f\noutputNumChannels: %ld\nbounceInterval: %i\nmaxBounceCount: %i\nstartupWatchdogPeriod: %i\nmaxPrebufferedByteCount: %i\nformat: %@\nbit rate: %f\nuserAgent: %@\ncacheDirectory: %@\npredefinedHttpHeaderValues: %@\ncacheEnabled: %@\nseekingFromCacheEnabled: %@\nautomaticAudioSessionHandlingEnabled: %@\nenableTimeAndPitchConversion: %@\nrequireStrictContentTypeChecking: %@\nmaxDiskCacheSize: %i\nusePrebufferSizeCalculationInSeconds: %@\nusePrebufferSizeCalculationInPackets: %@\nrequiredPrebufferSizeInSeconds: %f\nrequiredInitialPrebufferedByteCountForContinuousStream: %i\nrequiredInitialPrebufferedByteCountForNonContinuousStream: %i\nrequiredInitialPrebufferedPacketCount: %i",
- freeStreamerReleaseVersion(),
- self.url,
- self.configuration.bufferCount,
- self.configuration.bufferSize,
- self.configuration.maxPacketDescs,
- self.configuration.httpConnectionBufferSize,
- self.configuration.outputSampleRate,
- self.configuration.outputNumChannels,
- self.configuration.bounceInterval,
- self.configuration.maxBounceCount,
- self.configuration.startupWatchdogPeriod,
- self.configuration.maxPrebufferedByteCount,
- self.formatDescription,
- self.bitRate,
- self.configuration.userAgent,
- self.configuration.cacheDirectory,
- self.configuration.predefinedHttpHeaderValues,
- (self.configuration.cacheEnabled ? @"YES" : @"NO"),
- (self.configuration.seekingFromCacheEnabled ? @"YES" : @"NO"),
- (self.configuration.automaticAudioSessionHandlingEnabled ? @"YES" : @"NO"),
- (self.configuration.enableTimeAndPitchConversion ? @"YES" : @"NO"),
- (self.configuration.requireStrictContentTypeChecking ? @"YES" : @"NO"),
- self.configuration.maxDiskCacheSize,
- (self.configuration.usePrebufferSizeCalculationInSeconds ? @"YES" : @"NO"),
- (self.configuration.usePrebufferSizeCalculationInPackets ? @"YES" : @"NO"),
- self.configuration.requiredPrebufferSizeInSeconds,
- self.configuration.requiredInitialPrebufferedByteCountForContinuousStream,
- self.configuration.requiredInitialPrebufferedByteCountForNonContinuousStream,
- self.configuration.requiredInitialPrebufferedPacketCount];
- }
- @end
- /*
- * ===============================================================
- * FSAudioStream public implementation, merely wraps the
- * private class.
- * ===============================================================
- */
- @implementation FSAudioStream
- -(id)init
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.init needs to be called in the main thread");
-
- FSStreamConfiguration *defaultConfiguration = [[FSStreamConfiguration alloc] init];
-
- if (self = [self initWithConfiguration:defaultConfiguration]) {
- }
- return self;
- }
- - (id)initWithUrl:(NSURL *)url
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.initWithURL needs to be called in the main thread");
-
- if (self = [self init]) {
- _private.url = url;
- }
- return self;
- }
- - (id)initWithConfiguration:(FSStreamConfiguration *)configuration
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.initWithConfiguration needs to be called in the main thread");
-
- if (self = [super init]) {
- astreamer::Stream_Configuration *c = astreamer::Stream_Configuration::configuration();
-
- c->bufferCount = configuration.bufferCount;
- c->bufferSize = configuration.bufferSize;
- c->maxPacketDescs = configuration.maxPacketDescs;
- c->httpConnectionBufferSize = configuration.httpConnectionBufferSize;
- c->outputSampleRate = configuration.outputSampleRate;
- c->outputNumChannels = configuration.outputNumChannels;
- c->maxBounceCount = configuration.maxBounceCount;
- c->bounceInterval = configuration.bounceInterval;
- c->startupWatchdogPeriod = configuration.startupWatchdogPeriod;
- c->maxPrebufferedByteCount = configuration.maxPrebufferedByteCount;
- c->usePrebufferSizeCalculationInSeconds = configuration.usePrebufferSizeCalculationInSeconds;
- c->usePrebufferSizeCalculationInPackets = configuration.usePrebufferSizeCalculationInPackets;
- c->cacheEnabled = configuration.cacheEnabled;
- c->seekingFromCacheEnabled = configuration.seekingFromCacheEnabled;
- c->automaticAudioSessionHandlingEnabled = configuration.automaticAudioSessionHandlingEnabled;
- c->enableTimeAndPitchConversion = configuration.enableTimeAndPitchConversion;
- c->requireStrictContentTypeChecking = configuration.requireStrictContentTypeChecking;
- c->maxDiskCacheSize = configuration.maxDiskCacheSize;
- c->requiredInitialPrebufferedByteCountForContinuousStream = configuration.requiredInitialPrebufferedByteCountForContinuousStream;
- c->requiredInitialPrebufferedByteCountForNonContinuousStream = configuration.requiredInitialPrebufferedByteCountForNonContinuousStream;
- c->requiredPrebufferSizeInSeconds = configuration.requiredPrebufferSizeInSeconds;
- c->requiredInitialPrebufferedPacketCount = configuration.requiredInitialPrebufferedPacketCount;
-
- if (c->userAgent) {
- CFRelease(c->userAgent);
- }
- c->userAgent = CFStringCreateCopy(kCFAllocatorDefault, (__bridge CFStringRef)configuration.userAgent);
-
- if (c->cacheDirectory) {
- CFRelease(c->cacheDirectory);
- }
- if (configuration.cacheDirectory) {
- c->cacheDirectory = CFStringCreateCopy(kCFAllocatorDefault, (__bridge CFStringRef)configuration.cacheDirectory);
- } else {
- c->cacheDirectory = NULL;
- }
-
- if (c->predefinedHttpHeaderValues) {
- CFRelease(c->predefinedHttpHeaderValues);
- }
- if (configuration.predefinedHttpHeaderValues) {
- c->predefinedHttpHeaderValues = CFDictionaryCreateCopy(kCFAllocatorDefault, (__bridge CFDictionaryRef)configuration.predefinedHttpHeaderValues);
- } else {
- c->predefinedHttpHeaderValues = NULL;
- }
-
- _private = [[FSAudioStreamPrivate alloc] init];
- _private.stream = self;
- }
- return self;
- }
- - (void)dealloc
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.dealloc needs to be called in the main thread");
-
- AudioStreamStateObserver *observer = [_private streamStateObserver];
-
- // Break the cyclic loop so that dealloc() may be called
- observer->priv = nil;
-
- _private.stream = nil;
- _private.delegate = nil;
-
- _private = nil;
- }
- - (void)setUrl:(NSURL *)url
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setUrl needs to be called in the main thread");
-
- [_private setUrl:url];
- }
- - (NSURL*)url
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.url needs to be called in the main thread");
-
- return [_private url];
- }
- - (void)setStrictContentTypeChecking:(BOOL)strictContentTypeChecking
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setStrictContentTypeChecking needs to be called in the main thread");
-
- [_private setStrictContentTypeChecking:strictContentTypeChecking];
- }
- - (BOOL)strictContentTypeChecking
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.strictContentTypeChecking needs to be called in the main thread");
-
- return [_private strictContentTypeChecking];
- }
- - (NSURL*)outputFile
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.outputFile needs to be called in the main thread");
-
- return [_private outputFile];
- }
- - (void)setOutputFile:(NSURL *)outputFile
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setOutputFile needs to be called in the main thread");
-
- [_private setOutputFile:outputFile];
- }
- - (void)setDefaultContentType:(NSString *)defaultContentType
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setDefaultContentType needs to be called in the main thread");
-
- [_private setDefaultContentType:defaultContentType];
- }
- - (NSString*)defaultContentType
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.defaultContentType needs to be called in the main thread");
-
- return [_private defaultContentType];
- }
- - (NSString*)contentType
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.contentType needs to be called in the main thread");
-
- return [_private contentType];
- }
- - (NSString*)suggestedFileExtension
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.suggestedFileExtension needs to be called in the main thread");
-
- return [_private suggestedFileExtension];
- }
- - (UInt64)defaultContentLength
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.defaultContentLength needs to be called in the main thread");
-
- return [_private defaultContentLength];
- }
- - (UInt64)contentLength
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.contentLength needs to be called in the main thread");
-
- return [_private contentLength];
- }
- - (UInt64)audioDataByteCount
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.audioDataByteCount needs to be called in the main thread");
-
- return [_private audioDataByteCount];
- }
- - (void)preload
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.preload needs to be called in the main thread");
-
- [_private preload];
- }
- - (void)play
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.play needs to be called in the main thread");
-
- [_private play];
- }
- - (void)playFromURL:(NSURL*)url
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.playFromURL needs to be called in the main thread");
-
- [_private playFromURL:url];
- }
- - (void)playFromOffset:(FSSeekByteOffset)offset
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.playFromOffset needs to be called in the main thread");
-
- [_private playFromOffset:offset];
- }
- - (void)stop
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.stop needs to be called in the main thread");
-
- [_private stop];
- }
- - (void)pause
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.pause needs to be called in the main thread");
-
- [_private pause];
- }
- - (void)rewind:(unsigned int)seconds
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.rewind needs to be called in the main thread");
-
- [_private rewind:seconds];
- }
- - (void)seekToPosition:(FSStreamPosition)position
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.seekToPosition needs to be called in the main thread");
-
- if (!(position.position > 0)) {
- // To retain compatibility with older implementations,
- // fallback to using less accurate position.minute and position.second, if needed
- const float seekTime = position.minute * 60 + position.second;
-
- position.position = seekTime / [_private durationInSeconds];
- }
-
- [_private seekToOffset:position.position];
- }
- - (void)setPlayRate:(float)playRate
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setPlayRate needs to be called in the main thread");
-
- [_private setPlayRate:playRate];
- }
- - (BOOL)isPlaying
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.isPlaying needs to be called in the main thread");
-
- return [_private isPlaying];
- }
- - (void)expungeCache
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.expungeCache needs to be called in the main thread");
-
- [_private expungeCache];
- }
- - (NSUInteger)retryCount
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.retryCount needs to be called in the main thread");
-
- return _private.retryCount;
- }
- - (FSStreamStatistics *)statistics
- {
- return _private.statistics;
- }
- - (FSLevelMeterState)levels
- {
- return _private.levels;
- }
- - (FSStreamPosition)currentTimePlayed
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.currentTimePlayed needs to be called in the main thread");
-
- FSStreamPosition pos;
- pos.position = 0;
- pos.playbackTimeInSeconds = [_private playbackPosition].timePlayed;
- pos.minute = 0;
- pos.second = 0;
-
- const float durationInSeconds = [_private durationInSeconds];
-
- if (durationInSeconds > 0) {
- pos.position = pos.playbackTimeInSeconds / [_private durationInSeconds];
- }
-
- // Extract the minutes and seconds for convenience
- if (pos.playbackTimeInSeconds > 0) {
- unsigned u = pos.playbackTimeInSeconds;
- unsigned s,m;
-
- s = u % 60;
- u /= 60;
- m = u;
-
- pos.minute = m;
- pos.second = s;
- }
- return pos;
- }
- - (FSStreamPosition)duration
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.duration needs to be called in the main thread");
-
- FSStreamPosition pos;
- pos.minute = 0;
- pos.second = 0;
- pos.playbackTimeInSeconds = 0;
- pos.position = 0;
-
- const float durationInSeconds = [_private durationInSeconds];
-
- if (durationInSeconds > 0) {
- unsigned u = durationInSeconds;
-
- unsigned s,m;
-
- s = u % 60;
- u /= 60;
- m = u;
-
- pos.minute = m;
- pos.second = s;
- }
- pos.playbackTimeInSeconds = durationInSeconds;
- return pos;
- }
- - (FSSeekByteOffset)currentSeekByteOffset
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.currentSeekByteOffset needs to be called in the main thread");
-
- return _private.currentSeekByteOffset;
- }
- - (float)bitRate
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.bitRate needs to be called in the main thread");
-
- return _private.bitRate;
- }
- - (BOOL)continuous
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.continuous needs to be called in the main thread");
-
- return !([_private durationInSeconds] > 0);
- }
- - (BOOL)cached
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.cached needs to be called in the main thread");
-
- return _private.cached;
- }
- - (size_t)prebufferedByteCount
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.prebufferedByteCount needs to be called in the main thread");
-
- return _private.prebufferedByteCount;
- }
- - (float)volume
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.volume needs to be called in the main thread");
-
- return [_private currentVolume];
- }
- - (unsigned long long)totalCachedObjectsSize
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.totalCachedObjectsSize needs to be called in the main thread");
-
- return [_private totalCachedObjectsSize];
- }
- - (void)setVolume:(float)volume
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setVolume needs to be called in the main thread");
-
- [_private setVolume:volume];
- }
- - (void (^)())onCompletion
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.onCompletion needs to be called in the main thread");
-
- return _private.onCompletion;
- }
- - (void)setOnCompletion:(void (^)())onCompletion
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setOnCompletion needs to be called in the main thread");
-
- _private.onCompletion = onCompletion;
- }
- - (void (^)(FSAudioStreamState state))onStateChange
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.onStateChange needs to be called in the main thread");
-
- return _private.onStateChange;
- }
- - (void (^)(NSDictionary *metaData))onMetaDataAvailable
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.onMetaDataAvailable needs to be called in the main thread");
-
- return _private.onMetaDataAvailable;
- }
- - (void (^)(FSAudioStreamError error, NSString *errorDescription))onFailure
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.onFailure needs to be called in the main thread");
-
- return _private.onFailure;
- }
- - (void)setOnStateChange:(void (^)(FSAudioStreamState))onStateChange
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setOnStateChange needs to be called in the main thread");
-
- _private.onStateChange = onStateChange;
- }
- - (void)setOnMetaDataAvailable:(void (^)(NSDictionary *))onMetaDataAvailable
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setOnMetaDataAvailable needs to be called in the main thread");
-
- _private.onMetaDataAvailable = onMetaDataAvailable;
- }
- - (void)setOnFailure:(void (^)(FSAudioStreamError error, NSString *errorDescription))onFailure
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setOnFailure needs to be called in the main thread");
-
- _private.onFailure = onFailure;
- }
- - (FSStreamConfiguration *)configuration
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.configuration needs to be called in the main thread");
-
- return _private.configuration;
- }
- - (void)setDelegate:(id<FSPCMAudioStreamDelegate>)delegate
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setDelegate needs to be called in the main thread");
-
- _private.delegate = delegate;
- }
- - (id<FSPCMAudioStreamDelegate>)delegate
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.delegate needs to be called in the main thread");
-
- return _private.delegate;
- }
- -(NSString *)description
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.description needs to be called in the main thread");
-
- return [_private description];
- }
- -(NSUInteger)maxRetryCount
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.maxRetryCount needs to be called in the main thread");
-
- return [_private maxRetryCount];
- }
- -(void)setMaxRetryCount:(NSUInteger)maxRetryCount
- {
- NSAssert([NSThread isMainThread], @"FSAudioStream.setMaxRetryCount needs to be called in the main thread");
-
- [_private setMaxRetryCount:maxRetryCount];
- }
- @end
- /*
- * ===============================================================
- * AudioStreamStateObserver: listen to the state from the audio stream.
- * ===============================================================
- */
- void AudioStreamStateObserver::audioStreamErrorOccurred(int errorCode, CFStringRef errorDescription)
- {
- FSAudioStreamError error = kFsAudioStreamErrorNone;
-
- NSString *errorForObjC = @"";
-
- if (errorDescription) {
- errorForObjC = CFBridgingRelease(CFStringCreateCopy(kCFAllocatorDefault, errorDescription));
- }
-
- switch (errorCode) {
- case kFsAudioStreamErrorOpen:
- error = kFsAudioStreamErrorOpen;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Error opening the stream: %@ %@", errorForObjC, priv);
- #endif
-
- break;
- case kFsAudioStreamErrorStreamParse:
- error = kFsAudioStreamErrorStreamParse;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Error parsing the stream: %@ %@", errorForObjC, priv);
- #endif
-
- break;
- case kFsAudioStreamErrorNetwork:
- error = kFsAudioStreamErrorNetwork;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Network error: %@ %@", errorForObjC, priv);
- #endif
-
- break;
- case kFsAudioStreamErrorUnsupportedFormat:
- error = kFsAudioStreamErrorUnsupportedFormat;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Unsupported format error: %@ %@", errorForObjC, priv);
- #endif
-
- break;
-
- case kFsAudioStreamErrorStreamBouncing:
- error = kFsAudioStreamErrorStreamBouncing;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Stream bounced: %@ %@", errorForObjC, priv);
- #endif
-
- break;
-
- case kFsAudioStreamErrorTerminated:
- error = kFsAudioStreamErrorTerminated;
-
- #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
- NSLog(@"FSAudioStream: Stream terminated: %@ %@", errorForObjC, priv);
- #endif
- break;
-
- default:
- break;
- }
-
- if (priv.onFailure) {
- priv.onFailure(error, errorForObjC);
- }
-
- NSDictionary *userInfo = @{FSAudioStreamNotificationKey_Error: @(errorCode),
- FSAudioStreamNotificationKey_ErrorDescription: errorForObjC,
- FSAudioStreamNotificationKey_Stream: [NSValue valueWithPointer:source]};
- NSNotification *notification = [NSNotification notificationWithName:FSAudioStreamErrorNotification object:priv.stream userInfo:userInfo];
-
- [[NSNotificationCenter defaultCenter] postNotification:notification];
-
- if (error == kFsAudioStreamErrorNetwork ||
- error == kFsAudioStreamErrorUnsupportedFormat ||
- error == kFsAudioStreamErrorOpen ||
- error == kFsAudioStreamErrorTerminated) {
-
- if (!source->isPreloading()) {
- [priv attemptRestart];
- }
- }
- }
-
- void AudioStreamStateObserver::audioStreamStateChanged(astreamer::Audio_Stream::State state)
- {
- SEL notificationHandler;
-
- switch (state) {
- case astreamer::Audio_Stream::STOPPED:
- notificationHandler = @selector(notifyPlaybackStopped);
- break;
- case astreamer::Audio_Stream::BUFFERING:
- notificationHandler = @selector(notifyPlaybackBuffering);
- break;
- case astreamer::Audio_Stream::PLAYING:
- [priv endBackgroundTask];
-
- notificationHandler = @selector(notifyPlaybackPlaying);
- break;
- case astreamer::Audio_Stream::PAUSED:
- notificationHandler = @selector(notifyPlaybackPaused);
- break;
- case astreamer::Audio_Stream::SEEKING:
- notificationHandler = @selector(notifyPlaybackSeeking);
- break;
- case astreamer::Audio_Stream::END_OF_FILE:
- notificationHandler = @selector(notifyPlaybackEndOfFile);
- break;
- case astreamer::Audio_Stream::FAILED:
- [priv endBackgroundTask];
-
- notificationHandler = @selector(notifyPlaybackFailed);
- break;
- case astreamer::Audio_Stream::PLAYBACK_COMPLETED:
- notificationHandler = @selector(notifyPlaybackCompletion);
- break;
- default:
- // Unknown state
- notificationHandler = @selector(notifyPlaybackUnknownState);
- break;
- }
-
- // Detach from the player so that the event loop can complete its cycle.
- // This ensures that the stream gets closed, if needs to be.
- [NSTimer scheduledTimerWithTimeInterval:0
- target:priv
- selector:notificationHandler
- userInfo:nil
- repeats:NO];
- }
-
- void AudioStreamStateObserver::audioStreamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
- {
- NSMutableDictionary *metaDataDictionary = [[NSMutableDictionary alloc] init];
-
- for (std::map<CFStringRef,CFStringRef>::iterator iter = metaData.begin(); iter != metaData.end(); ++iter) {
- CFStringRef key = iter->first;
- CFStringRef value = iter->second;
-
- metaDataDictionary[CFBridgingRelease(key)] = CFBridgingRelease(value);
- }
-
- if (priv.onMetaDataAvailable) {
- priv.onMetaDataAvailable(metaDataDictionary);
- }
-
- NSDictionary *userInfo = @{FSAudioStreamNotificationKey_MetaData: metaDataDictionary,
- FSAudioStreamNotificationKey_Stream: [NSValue valueWithPointer:source]};
- NSNotification *notification = [NSNotification notificationWithName:FSAudioStreamMetaDataNotification object:priv.stream userInfo:userInfo];
-
- [[NSNotificationCenter defaultCenter] postNotification:notification];
- }
- void AudioStreamStateObserver::samplesAvailable(AudioBufferList *samples, UInt32 frames, AudioStreamPacketDescription description)
- {
- if ([priv.delegate respondsToSelector:@selector(audioStream:samplesAvailable:frames:description:)]) {
- [priv.delegate audioStream:priv.stream samplesAvailable:samples frames:frames description:description];
- }
- }
- void AudioStreamStateObserver::bitrateAvailable()
- {
- [priv bitrateAvailable];
- }
|