B面1.0.5再优化版本,处理启动页加载时间过长,无法迅速确定前往A/B面结果/首页资源加载时间过长
This commit is contained in:
parent
716180557b
commit
63078adc81
3
Podfile
3
Podfile
@ -23,10 +23,9 @@ pod 'JXSegmentedView'
|
||||
pod 'JXPagingView/Paging'
|
||||
#刷新支持
|
||||
pod 'MJRefresh'
|
||||
#流音频播放
|
||||
pod 'FreeStreamer'
|
||||
#下载按钮
|
||||
pod "DownloadButton"
|
||||
#下载框架
|
||||
pod 'Tiercel'
|
||||
|
||||
end
|
||||
|
||||
10
Podfile.lock
10
Podfile.lock
@ -1,14 +1,11 @@
|
||||
PODS:
|
||||
- Alamofire (5.9.1)
|
||||
- DownloadButton (0.1.0)
|
||||
- FreeStreamer (4.0.0):
|
||||
- Reachability (~> 3.0)
|
||||
- IQKeyboardManagerSwift (6.5.16)
|
||||
- JXPagingView/Paging (2.1.3)
|
||||
- JXSegmentedView (1.3.3)
|
||||
- Kingfisher (7.11.0)
|
||||
- MJRefresh (3.7.9)
|
||||
- Reachability (3.7.6)
|
||||
- SnapKit (5.7.1)
|
||||
- SVProgressHUD (2.3.1):
|
||||
- SVProgressHUD/Core (= 2.3.1)
|
||||
@ -19,7 +16,6 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- DownloadButton
|
||||
- FreeStreamer
|
||||
- IQKeyboardManagerSwift
|
||||
- JXPagingView/Paging
|
||||
- JXSegmentedView
|
||||
@ -34,13 +30,11 @@ SPEC REPOS:
|
||||
trunk:
|
||||
- Alamofire
|
||||
- DownloadButton
|
||||
- FreeStreamer
|
||||
- IQKeyboardManagerSwift
|
||||
- JXPagingView
|
||||
- JXSegmentedView
|
||||
- Kingfisher
|
||||
- MJRefresh
|
||||
- Reachability
|
||||
- SnapKit
|
||||
- SVProgressHUD
|
||||
- SwiftDate
|
||||
@ -49,18 +43,16 @@ SPEC REPOS:
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
|
||||
DownloadButton: 49a21a89e0d7d1b42d9134f79aaa40e727cd57c3
|
||||
FreeStreamer: 7e9c976045701ac2f7e9c14c17245203c37bf2ea
|
||||
IQKeyboardManagerSwift: 12d89768845bb77b55cc092ecc2b1f9370f06b76
|
||||
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
|
||||
JXSegmentedView: 651b60fcf705258ba9395edd53876dbd2853fb68
|
||||
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
|
||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||
Reachability: fd0ecd23705e2599e4cceeb943222ae02296cbc6
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22
|
||||
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
|
||||
Tiercel: c0a73f876a72800333b15f4e7e48791f4ad21e90
|
||||
|
||||
PODFILE CHECKSUM: 2aaf13c9ed7d2b78bcf21030bbbacf03b3a0212c
|
||||
PODFILE CHECKSUM: 436104abc66aacc2c16f90adefe5230845e81453
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@ -1,274 +0,0 @@
|
||||
/*
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
#include "FSAudioStream.h"
|
||||
|
||||
@class FSCheckContentTypeRequest;
|
||||
@class FSParsePlaylistRequest;
|
||||
@class FSParseRssPodcastFeedRequest;
|
||||
@class FSPlaylistItem;
|
||||
@protocol FSAudioControllerDelegate;
|
||||
|
||||
/**
|
||||
* FSAudioController is functionally equivalent to FSAudioStream with
|
||||
* one addition: it can be directly fed with a playlist (PLS, M3U) URL
|
||||
* or an RSS podcast feed. It determines the content type and forms
|
||||
* a playlist for playback. Notice that this generates more traffic and
|
||||
* is generally more slower than using an FSAudioStream directly.
|
||||
*
|
||||
* It is also possible to construct a playlist by yourself by providing
|
||||
* the playlist items. In this case see the methods for managing the playlist.
|
||||
*
|
||||
* If you have a playlist with multiple items, FSAudioController attemps
|
||||
* automatically preload the next item in the playlist. This helps to
|
||||
* start the next item playback immediately without the need for the
|
||||
* user to wait for buffering.
|
||||
*
|
||||
* Notice that do not attempt to set your own blocks to the audio stream
|
||||
* owned by the controller. FSAudioController uses the blocks internally
|
||||
* and any user set blocks will be overwritten. Instead use the blocks
|
||||
* offered by FSAudioController.
|
||||
*/
|
||||
@interface FSAudioController : NSObject {
|
||||
NSURL *_url;
|
||||
NSMutableArray *_streams;
|
||||
|
||||
float _volume;
|
||||
|
||||
BOOL _readyToPlay;
|
||||
|
||||
FSCheckContentTypeRequest *_checkContentTypeRequest;
|
||||
FSParsePlaylistRequest *_parsePlaylistRequest;
|
||||
FSParseRssPodcastFeedRequest *_parseRssPodcastFeedRequest;
|
||||
|
||||
void (^_onStateChangeBlock)(FSAudioStreamState);
|
||||
void (^_onMetaDataAvailableBlock)(NSDictionary*);
|
||||
void (^_onFailureBlock)(FSAudioStreamError error, NSString *errorDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the audio stream with an URL.
|
||||
*
|
||||
* @param url The URL from which the stream data is retrieved.
|
||||
*/
|
||||
- (id)initWithUrl:(NSURL *)url;
|
||||
|
||||
/**
|
||||
* Starts playing the stream. Before the playback starts,
|
||||
* the URL content type is checked and playlists resolved.
|
||||
*/
|
||||
- (void)play;
|
||||
|
||||
/**
|
||||
* Starts playing the stream from an URL. Before the playback starts,
|
||||
* the URL content type is checked and playlists resolved.
|
||||
*
|
||||
* @param url The URL from which the stream data is retrieved.
|
||||
*/
|
||||
- (void)playFromURL:(NSURL *)url;
|
||||
|
||||
/**
|
||||
* Starts playing the stream from the given playlist. Each item in the array
|
||||
* must an FSPlaylistItem.
|
||||
*
|
||||
* @param playlist The playlist items.
|
||||
*/
|
||||
- (void)playFromPlaylist:(NSArray *)playlist;
|
||||
|
||||
/**
|
||||
* Starts playing the stream from the given playlist. Each item in the array
|
||||
* must an FSPlaylistItem. The playback starts from the given index
|
||||
* in the playlist.
|
||||
*
|
||||
* @param playlist The playlist items.
|
||||
* @param index The playlist index where to start playback from.
|
||||
*/
|
||||
- (void)playFromPlaylist:(NSArray *)playlist itemIndex:(NSUInteger)index;
|
||||
|
||||
/**
|
||||
* Plays a playlist item at the specified index.
|
||||
*
|
||||
* @param index The playlist index where to start playback from.
|
||||
*/
|
||||
- (void)playItemAtIndex:(NSUInteger)index;
|
||||
|
||||
/**
|
||||
* Returns the count of playlist items.
|
||||
*/
|
||||
- (NSUInteger)countOfItems;
|
||||
|
||||
/**
|
||||
* Adds an item to the playlist.
|
||||
*
|
||||
* @param item The playlist item to be added.
|
||||
*/
|
||||
- (void)addItem:(FSPlaylistItem *)item;
|
||||
|
||||
/**
|
||||
* Adds an item to the playlist at a specific position.
|
||||
*
|
||||
* @param item The playlist item to be added.
|
||||
* @param index The location in the playlist to place the new item
|
||||
*/
|
||||
- (void)insertItem:(FSPlaylistItem *)item atIndex:(NSInteger)index;
|
||||
|
||||
/**
|
||||
* Moves an item already in the playlist to a different position in the playlist
|
||||
*
|
||||
* @param from The original index of the track to move
|
||||
* @param to The destination of the the track at the index specified in `from`
|
||||
*/
|
||||
- (void)moveItemAtIndex:(NSUInteger)from toIndex:(NSUInteger)to;
|
||||
|
||||
/**
|
||||
* Replaces a playlist item.
|
||||
*
|
||||
* @param index The index of the playlist item to be replaced.
|
||||
* @param item The playlist item used the replace the existing one.
|
||||
*/
|
||||
- (void)replaceItemAtIndex:(NSUInteger)index withItem:(FSPlaylistItem *)item;
|
||||
|
||||
/**
|
||||
* Removes a playlist item.
|
||||
*
|
||||
* @param index The index of the playlist item to be removed.
|
||||
*/
|
||||
- (void)removeItemAtIndex:(NSUInteger)index;
|
||||
|
||||
/**
|
||||
* Stops the stream playback.
|
||||
*/
|
||||
- (void)stop;
|
||||
|
||||
/**
|
||||
* If the stream is playing, the stream playback is paused upon calling pause.
|
||||
* Otherwise (the stream is paused), calling pause will continue the playback.
|
||||
*/
|
||||
- (void)pause;
|
||||
|
||||
/**
|
||||
* Returns the playback status: YES if the stream is playing, NO otherwise.
|
||||
*/
|
||||
- (BOOL)isPlaying;
|
||||
|
||||
/**
|
||||
* Returns if the current multiple-item playlist has next item
|
||||
*/
|
||||
- (BOOL)hasNextItem;
|
||||
|
||||
/**
|
||||
* Returns if the current multiple-item playlist has Previous item
|
||||
*/
|
||||
- (BOOL)hasPreviousItem;
|
||||
|
||||
/**
|
||||
* Play the next item of multiple-item playlist
|
||||
*/
|
||||
- (void)playNextItem;
|
||||
|
||||
/**
|
||||
* Play the previous item of multiple-item playlist
|
||||
*/
|
||||
- (void)playPreviousItem;
|
||||
|
||||
/**
|
||||
* This property holds the current playback volume of the stream,
|
||||
* from 0.0 to 1.0.
|
||||
*
|
||||
* Note that the overall volume is still constrained by the volume
|
||||
* set by the user! So the actual volume cannot be higher
|
||||
* than the volume currently set by the user. For example, if
|
||||
* requesting a volume of 0.5, then the volume will be 50%
|
||||
* lower than the current playback volume set by the user.
|
||||
*/
|
||||
@property (nonatomic,assign) float volume;
|
||||
/**
|
||||
* The controller URL.
|
||||
*/
|
||||
@property (nonatomic,assign) NSURL *url;
|
||||
/**
|
||||
* The the active playing stream, which may change
|
||||
* from time to time during the playback. In this way, do not
|
||||
* set your own blocks to the stream but use the blocks
|
||||
* provides by FSAudioController.
|
||||
*/
|
||||
@property (readonly) FSAudioStream *activeStream;
|
||||
/**
|
||||
* The playlist item the controller is currently using.
|
||||
*/
|
||||
@property (nonatomic,readonly) FSPlaylistItem *currentPlaylistItem;
|
||||
|
||||
/**
|
||||
* This property determines if the next playlist item should be loaded
|
||||
* automatically. This is YES by default.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL preloadNextPlaylistItemAutomatically;
|
||||
|
||||
/**
|
||||
* This property determines if the debug output is enabled. Disabled
|
||||
* by default
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL enableDebugOutput;
|
||||
|
||||
/**
|
||||
* This property determines if automatic audio session handling is enabled.
|
||||
* This is YES by default.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL automaticAudioSessionHandlingEnabled;
|
||||
|
||||
/**
|
||||
* This property holds the configuration used for the streaming.
|
||||
*/
|
||||
@property (nonatomic,strong) FSStreamConfiguration *configuration;
|
||||
|
||||
/**
|
||||
* Called upon a state change.
|
||||
*/
|
||||
@property (copy) void (^onStateChange)(FSAudioStreamState state);
|
||||
/**
|
||||
* Called upon a meta data is available.
|
||||
*/
|
||||
@property (copy) void (^onMetaDataAvailable)(NSDictionary *metadata);
|
||||
/**
|
||||
* Called upon a failure.
|
||||
*/
|
||||
@property (copy) void (^onFailure)(FSAudioStreamError error, NSString *errorDescription);
|
||||
|
||||
/**
|
||||
* Delegate.
|
||||
*/
|
||||
@property (nonatomic,unsafe_unretained) IBOutlet id<FSAudioControllerDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* To check the preloading status, use this delegate.
|
||||
*/
|
||||
@protocol FSAudioControllerDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
/**
|
||||
* Called when the controller wants to start preloading an item. Return YES or NO
|
||||
* depending if you want this item to be preloaded.
|
||||
*
|
||||
* @param audioController The audio controller which is doing the preloading.
|
||||
* @param stream The stream which is wanted to be preloaded.
|
||||
*/
|
||||
- (BOOL)audioController:(FSAudioController *)audioController allowPreloadingForStream:(FSAudioStream *)stream;
|
||||
|
||||
/**
|
||||
* Called when the controller starts to preload an item.
|
||||
*
|
||||
* @param audioController The audio controller which is doing the preloading.
|
||||
* @param stream The stream which is preloaded.
|
||||
*/
|
||||
- (void)audioController:(FSAudioController *)audioController preloadStartedForStream:(FSAudioStream *)stream;
|
||||
@end
|
||||
@ -1,875 +0,0 @@
|
||||
/*
|
||||
* 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 "FSAudioController.h"
|
||||
#import "FSPlaylistItem.h"
|
||||
#import "FSCheckContentTypeRequest.h"
|
||||
#import "FSParsePlaylistRequest.h"
|
||||
#import "FSParseRssPodcastFeedRequest.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
/**
|
||||
* Private interface for FSAudioController.
|
||||
*/
|
||||
@interface FSAudioController ()
|
||||
- (void)notifyRetrievingURL;
|
||||
|
||||
@property (readonly) FSAudioStream *audioStream;
|
||||
@property (readonly) FSCheckContentTypeRequest *checkContentTypeRequest;
|
||||
@property (readonly) FSParsePlaylistRequest *parsePlaylistRequest;
|
||||
@property (readonly) FSParseRssPodcastFeedRequest *parseRssPodcastFeedRequest;
|
||||
@property (nonatomic,assign) BOOL readyToPlay;
|
||||
@property (nonatomic,assign) NSUInteger currentPlaylistItemIndex;
|
||||
@property (nonatomic,strong) NSMutableArray *playlistItems;
|
||||
@property (nonatomic,strong) NSMutableArray *streams;
|
||||
@property (nonatomic,assign) BOOL needToSetVolume;
|
||||
@property (nonatomic,assign) BOOL songSwitchInProgress;
|
||||
@property (nonatomic,assign) float outputVolume;
|
||||
|
||||
- (void)audioStreamStateDidChange:(NSNotification *)notification;
|
||||
- (void)deactivateInactivateStreams:(NSUInteger)currentActiveStream;
|
||||
- (void)setAudioSessionActive:(BOOL)active;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Acts as a proxy object for FSAudioStream. Lazily initializes
|
||||
* the stream when it is needed.
|
||||
*
|
||||
* A call to deactivate releases the stream.
|
||||
*/
|
||||
@interface FSAudioStreamProxy : NSObject {
|
||||
FSAudioStream *_audioStream;
|
||||
}
|
||||
|
||||
@property (readonly) FSAudioStream *audioStream;
|
||||
@property (nonatomic,copy) NSURL *url;
|
||||
@property (nonatomic,weak) FSAudioController *audioController;
|
||||
|
||||
- (void)deactivate;
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* FSAudioStreamProxy implementation.
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
@implementation FSAudioStreamProxy
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithAudioController:(FSAudioController *)controller
|
||||
{
|
||||
if (self = [self init]) {
|
||||
self.audioController = controller;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (self.audioController.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] FSAudioStreamProxy.dealloc: %@", __LINE__, self.url);
|
||||
}
|
||||
|
||||
[self deactivate];
|
||||
}
|
||||
|
||||
- (FSAudioStream *)audioStream
|
||||
{
|
||||
if (!_audioStream) {
|
||||
FSStreamConfiguration *conf;
|
||||
if (self.audioController.configuration) {
|
||||
conf = self.audioController.configuration;
|
||||
} else {
|
||||
conf = [[FSStreamConfiguration alloc] init];
|
||||
}
|
||||
|
||||
// Disable audio session handling for the audio stream; audio controller handles it
|
||||
conf.automaticAudioSessionHandlingEnabled = NO;
|
||||
|
||||
_audioStream = [[FSAudioStream alloc] initWithConfiguration:conf];
|
||||
|
||||
if (self.audioController.needToSetVolume) {
|
||||
_audioStream.volume = self.audioController.outputVolume;
|
||||
}
|
||||
|
||||
if (self.url) {
|
||||
_audioStream.url = self.url;
|
||||
}
|
||||
}
|
||||
return _audioStream;
|
||||
}
|
||||
|
||||
- (void)deactivate
|
||||
{
|
||||
[_audioStream stop];
|
||||
|
||||
_audioStream = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* FSAudioController implementation
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
@implementation FSAudioController
|
||||
|
||||
-(id)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_url = nil;
|
||||
_checkContentTypeRequest = nil;
|
||||
_parsePlaylistRequest = nil;
|
||||
_readyToPlay = NO;
|
||||
_playlistItems = [[NSMutableArray alloc] init];
|
||||
_streams = [[NSMutableArray alloc] init];
|
||||
self.preloadNextPlaylistItemAutomatically = YES;
|
||||
self.enableDebugOutput = NO;
|
||||
self.automaticAudioSessionHandlingEnabled = YES;
|
||||
self.configuration = [[FSStreamConfiguration alloc] init];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(audioStreamStateDidChange:)
|
||||
name:FSAudioStreamStateChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithUrl:(NSURL *)url
|
||||
{
|
||||
if (self = [self init]) {
|
||||
self.url = url;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
[_checkContentTypeRequest cancel];
|
||||
[_parsePlaylistRequest cancel];
|
||||
[_parseRssPodcastFeedRequest cancel];
|
||||
|
||||
for (FSAudioStreamProxy *proxy in _streams) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] dealloc. Deactivating stream %@", __LINE__, proxy.url);
|
||||
}
|
||||
|
||||
[proxy deactivate];
|
||||
}
|
||||
|
||||
[self setAudioSessionActive:NO];
|
||||
}
|
||||
|
||||
- (void)audioStreamStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
if (notification.object == self) {
|
||||
// URL retrieving notification from ourselves, ignore
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(notification.object == self.audioStream)) {
|
||||
// This doesn't concern us, return
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *dict = [notification userInfo];
|
||||
int state = [[dict valueForKey:FSAudioStreamNotificationKey_State] intValue];
|
||||
|
||||
if (state == kFSAudioStreamEndOfFile) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] EOF reached for %@", __LINE__, self.audioStream.url);
|
||||
}
|
||||
|
||||
if (!self.preloadNextPlaylistItemAutomatically) {
|
||||
// No preloading wanted, skip
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] Preloading disabled, return.", __LINE__);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Reached EOF for this stream, do we have another item waiting in the playlist?
|
||||
if ([self hasNextItem]) {
|
||||
FSAudioStreamProxy *proxy = [_streams objectAtIndex:self.currentPlaylistItemIndex + 1];
|
||||
FSAudioStream *nextStream = proxy.audioStream;
|
||||
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] Preloading %@", __LINE__, nextStream.url);
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(audioController:allowPreloadingForStream:)]) {
|
||||
if ([self.delegate audioController:self allowPreloadingForStream:nextStream]) {
|
||||
[nextStream preload];
|
||||
} else {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] Preloading disallowed for stream %@", __LINE__, nextStream.url);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Start preloading the next stream; we can load this as there is no override
|
||||
[nextStream preload];
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(audioController:preloadStartedForStream:)]) {
|
||||
[self.delegate audioController:self preloadStartedForStream:nextStream];
|
||||
}
|
||||
}
|
||||
} else if (state == kFsAudioStreamStopped && !self.songSwitchInProgress) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"Stream %@ stopped. No next playlist items. Deactivating audio session", self.audioStream.url);
|
||||
}
|
||||
|
||||
[self setAudioSessionActive:NO];
|
||||
} else if (state == kFsAudioStreamPlaybackCompleted && [self hasNextItem]) {
|
||||
self.currentPlaylistItemIndex = self.currentPlaylistItemIndex + 1;
|
||||
self.songSwitchInProgress = YES;
|
||||
|
||||
[self play];
|
||||
} else if (state == kFsAudioStreamFailed) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"Stream %@ failed. Deactivating audio session", self.audioStream.url);
|
||||
}
|
||||
|
||||
[self setAudioSessionActive:NO];
|
||||
} else if (state == kFsAudioStreamBuffering) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"Stream buffering. Activating audio session");
|
||||
}
|
||||
|
||||
self.songSwitchInProgress = NO;
|
||||
|
||||
if (self.automaticAudioSessionHandlingEnabled) {
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
|
||||
#endif
|
||||
}
|
||||
[self setAudioSessionActive:YES];
|
||||
} else if (state == kFsAudioStreamPlaying) {
|
||||
self.currentPlaylistItem.audioDataByteCount = self.activeStream.audioDataByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deactivateInactivateStreams:(NSUInteger)currentActiveStream
|
||||
{
|
||||
NSUInteger streamIndex = 0;
|
||||
|
||||
for (FSAudioStreamProxy *proxy in _streams) {
|
||||
if (streamIndex != currentActiveStream) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] Deactivating stream %@", __LINE__, proxy.url);
|
||||
}
|
||||
|
||||
[proxy deactivate];
|
||||
}
|
||||
streamIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAudioSessionActive:(BOOL)active
|
||||
{
|
||||
if (self.automaticAudioSessionHandlingEnabled) {
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
|
||||
[[AVAudioSession sharedInstance] setActive:active withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
|
||||
#else
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
|
||||
[[AVAudioSession sharedInstance] setActive:active error:nil];
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Properties
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (FSAudioStream *)audioStream
|
||||
{
|
||||
FSAudioStream *stream = nil;
|
||||
|
||||
if ([_streams count] == 0) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] Stream count %lu, creating a proxy object", __LINE__, (unsigned long)[_streams count]);
|
||||
}
|
||||
|
||||
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
|
||||
[_streams addObject:proxy];
|
||||
}
|
||||
|
||||
FSAudioStreamProxy *proxy = [_streams objectAtIndex:self.currentPlaylistItemIndex];
|
||||
|
||||
stream = proxy.audioStream;
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
- (FSCheckContentTypeRequest *)checkContentTypeRequest
|
||||
{
|
||||
if (!_checkContentTypeRequest) {
|
||||
__weak FSAudioController *weakSelf = self;
|
||||
|
||||
_checkContentTypeRequest = [[FSCheckContentTypeRequest alloc] init];
|
||||
_checkContentTypeRequest.url = self.url;
|
||||
_checkContentTypeRequest.onCompletion = ^() {
|
||||
if (weakSelf.checkContentTypeRequest.playlist) {
|
||||
// The URL is a playlist; retrieve the contents
|
||||
[weakSelf.parsePlaylistRequest start];
|
||||
} else if (weakSelf.checkContentTypeRequest.xml) {
|
||||
// The URL may be an RSS feed, check the contents
|
||||
[weakSelf.parseRssPodcastFeedRequest start];
|
||||
} else {
|
||||
// Not a playlist; try directly playing the URL
|
||||
|
||||
weakSelf.readyToPlay = YES;
|
||||
[weakSelf play];
|
||||
}
|
||||
};
|
||||
_checkContentTypeRequest.onFailure = ^() {
|
||||
// Failed to check the format; try playing anyway
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSAudioController: Failed to check the format, trying to play anyway, URL: %@", weakSelf.audioStream.url);
|
||||
#endif
|
||||
|
||||
weakSelf.readyToPlay = YES;
|
||||
[weakSelf play];
|
||||
};
|
||||
}
|
||||
return _checkContentTypeRequest;
|
||||
}
|
||||
|
||||
- (FSParsePlaylistRequest *)parsePlaylistRequest
|
||||
{
|
||||
if (!_parsePlaylistRequest) {
|
||||
__weak FSAudioController *weakSelf = self;
|
||||
|
||||
_parsePlaylistRequest = [[FSParsePlaylistRequest alloc] init];
|
||||
_parsePlaylistRequest.onCompletion = ^() {
|
||||
[weakSelf playFromPlaylist:weakSelf.parsePlaylistRequest.playlistItems];
|
||||
};
|
||||
_parsePlaylistRequest.onFailure = ^() {
|
||||
// Failed to parse the playlist; try playing anyway
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSAudioController: Playlist parsing failed, trying to play anyway, URL: %@", weakSelf.audioStream.url);
|
||||
#endif
|
||||
|
||||
weakSelf.readyToPlay = YES;
|
||||
[weakSelf play];
|
||||
};
|
||||
}
|
||||
return _parsePlaylistRequest;
|
||||
}
|
||||
|
||||
- (FSParseRssPodcastFeedRequest *)parseRssPodcastFeedRequest
|
||||
{
|
||||
if (!_parseRssPodcastFeedRequest) {
|
||||
__weak FSAudioController *weakSelf = self;
|
||||
|
||||
_parseRssPodcastFeedRequest = [[FSParseRssPodcastFeedRequest alloc] init];
|
||||
_parseRssPodcastFeedRequest.onCompletion = ^() {
|
||||
[weakSelf playFromPlaylist:weakSelf.parseRssPodcastFeedRequest.playlistItems];
|
||||
};
|
||||
_parseRssPodcastFeedRequest.onFailure = ^() {
|
||||
// Failed to parse the XML file; try playing anyway
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSAudioController: Failed to parse the RSS feed, trying to play anyway, URL: %@", weakSelf.audioStream.url);
|
||||
#endif
|
||||
|
||||
weakSelf.readyToPlay = YES;
|
||||
[weakSelf play];
|
||||
};
|
||||
}
|
||||
return _parseRssPodcastFeedRequest;
|
||||
}
|
||||
|
||||
- (void)notifyRetrievingURL
|
||||
{
|
||||
if (self.onStateChange) {
|
||||
self.onStateChange(kFsAudioStreamRetrievingURL);
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isPlaying
|
||||
{
|
||||
return [self.audioStream isPlaying];
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Public interface
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (void)play
|
||||
{
|
||||
if (!self.readyToPlay) {
|
||||
/*
|
||||
* Not ready to play; start by checking the content type of the given
|
||||
* URL.
|
||||
*/
|
||||
[self.checkContentTypeRequest start];
|
||||
|
||||
NSDictionary *userInfo = @{FSAudioStreamNotificationKey_State: @(kFsAudioStreamRetrievingURL)};
|
||||
NSNotification *notification = [NSNotification notificationWithName:FSAudioStreamStateChangeNotification object:self userInfo:userInfo];
|
||||
[[NSNotificationCenter defaultCenter] postNotification:notification];
|
||||
|
||||
[NSTimer scheduledTimerWithTimeInterval:0
|
||||
target:self
|
||||
selector:@selector(notifyRetrievingURL)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.playlistItems count] > 0) {
|
||||
if (self.currentPlaylistItem.originatingUrl) {
|
||||
self.audioStream.url = self.currentPlaylistItem.originatingUrl;
|
||||
} else {
|
||||
self.audioStream.url = self.currentPlaylistItem.url;
|
||||
}
|
||||
} else {
|
||||
self.audioStream.url = self.url;
|
||||
}
|
||||
|
||||
if (self.onStateChange) {
|
||||
self.audioStream.onStateChange = self.onStateChange;
|
||||
}
|
||||
if (self.onMetaDataAvailable) {
|
||||
self.audioStream.onMetaDataAvailable = self.onMetaDataAvailable;
|
||||
}
|
||||
if (self.onFailure) {
|
||||
self.audioStream.onFailure = self.onFailure;
|
||||
}
|
||||
|
||||
FSAudioStream *stream = self.audioStream;
|
||||
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"Playing %@", stream);
|
||||
}
|
||||
|
||||
[stream play];
|
||||
}
|
||||
|
||||
- (void)playFromURL:(NSURL*)url
|
||||
{
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_playlistItems removeAllObjects];
|
||||
|
||||
[self stop];
|
||||
|
||||
self.url = url;
|
||||
|
||||
[self play];
|
||||
}
|
||||
|
||||
- (void)playFromPlaylist:(NSArray *)playlist
|
||||
{
|
||||
[self playFromPlaylist:playlist itemIndex:0];
|
||||
}
|
||||
|
||||
- (void)playFromPlaylist:(NSArray *)playlist itemIndex:(NSUInteger)index
|
||||
{
|
||||
[self stop];
|
||||
|
||||
self.playlistItems = [[NSMutableArray alloc] init];
|
||||
_streams = [[NSMutableArray alloc] init];
|
||||
|
||||
self.currentPlaylistItemIndex = 0;
|
||||
|
||||
[self.playlistItems addObjectsFromArray:playlist];
|
||||
|
||||
for (FSPlaylistItem *item in playlist) {
|
||||
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
|
||||
proxy.url = item.url;
|
||||
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] playFromPlaylist. Adding stream proxy for %@", __LINE__, proxy.url);
|
||||
}
|
||||
|
||||
[_streams addObject:proxy];
|
||||
}
|
||||
|
||||
[self playItemAtIndex:index];
|
||||
}
|
||||
|
||||
- (void)playItemAtIndex:(NSUInteger)index
|
||||
{
|
||||
NSUInteger count = [self countOfItems];
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index >= count) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.audioStream stop];
|
||||
|
||||
self.currentPlaylistItemIndex = index;
|
||||
|
||||
self.readyToPlay = YES;
|
||||
|
||||
[self deactivateInactivateStreams:index];
|
||||
|
||||
[self play];
|
||||
}
|
||||
|
||||
- (NSUInteger)countOfItems
|
||||
{
|
||||
return [self.playlistItems count];
|
||||
}
|
||||
|
||||
- (void)addItem:(FSPlaylistItem *)item
|
||||
{
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.playlistItems addObject:item];
|
||||
|
||||
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
|
||||
proxy.url = item.url;
|
||||
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] addItem. Adding stream proxy for %@", __LINE__, proxy.url);
|
||||
}
|
||||
|
||||
[_streams addObject:proxy];
|
||||
}
|
||||
|
||||
- (void)insertItem:(FSPlaylistItem *)item atIndex:(NSInteger)index
|
||||
{
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > self.playlistItems.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self.playlistItems.count == 0 && index == 0) {
|
||||
[self addItem:item];
|
||||
return;
|
||||
}
|
||||
|
||||
[self.playlistItems insertObject:item
|
||||
atIndex:index];
|
||||
|
||||
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
|
||||
proxy.url = item.url;
|
||||
|
||||
[_streams insertObject:proxy
|
||||
atIndex:index];
|
||||
|
||||
if(index <= self.currentPlaylistItemIndex) {
|
||||
_currentPlaylistItemIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)replaceItemAtIndex:(NSUInteger)index withItem:(FSPlaylistItem *)item
|
||||
{
|
||||
NSUInteger count = [self countOfItems];
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index >= count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.currentPlaylistItemIndex == index) {
|
||||
// If the item is currently playing, do not allow the replacement
|
||||
return;
|
||||
}
|
||||
|
||||
[self.playlistItems replaceObjectAtIndex:index withObject:item];
|
||||
|
||||
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
|
||||
proxy.url = item.url;
|
||||
|
||||
[_streams replaceObjectAtIndex:index withObject:proxy];
|
||||
}
|
||||
|
||||
- (void)moveItemAtIndex:(NSUInteger)from toIndex:(NSUInteger)to {
|
||||
NSUInteger count = [self countOfItems];
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (from >= count || to >= count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(from == self.currentPlaylistItemIndex) {
|
||||
_currentPlaylistItemIndex = to;
|
||||
}
|
||||
else if(from < self.currentPlaylistItemIndex && to > self.currentPlaylistItemIndex) {
|
||||
_currentPlaylistItemIndex--;
|
||||
}
|
||||
else if(from > self.currentPlaylistItemIndex && to <= self.currentPlaylistItemIndex) {
|
||||
_currentPlaylistItemIndex++;
|
||||
}
|
||||
|
||||
id object = [self.playlistItems objectAtIndex:from];
|
||||
[self.playlistItems removeObjectAtIndex:from];
|
||||
[self.playlistItems insertObject:object atIndex:to];
|
||||
|
||||
id obj = [_streams objectAtIndex:from];
|
||||
[_streams removeObjectAtIndex:from];
|
||||
[_streams insertObject:obj atIndex:to];
|
||||
}
|
||||
|
||||
- (void)removeItemAtIndex:(NSUInteger)index
|
||||
{
|
||||
NSUInteger count = [self countOfItems];
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index >= count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.currentPlaylistItemIndex == index && self.isPlaying) {
|
||||
// If the item is currently playing, do not allow the removal
|
||||
return;
|
||||
}
|
||||
|
||||
FSPlaylistItem *current = self.currentPlaylistItem;
|
||||
|
||||
[self.playlistItems removeObjectAtIndex:index];
|
||||
|
||||
if (self.enableDebugOutput) {
|
||||
FSAudioStreamProxy *proxy = [_streams objectAtIndex:index];
|
||||
NSLog(@"[FSAudioController.m:%i] removeItemAtIndex. Removing stream proxy %@", __LINE__, proxy.url);
|
||||
}
|
||||
|
||||
[_streams removeObjectAtIndex:index];
|
||||
|
||||
// Update the current playlist item to be correct after the removal
|
||||
NSUInteger itemIndex = 0;
|
||||
for (FSPlaylistItem *item in self.playlistItems) {
|
||||
if (item == current) {
|
||||
self.currentPlaylistItemIndex = itemIndex;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
itemIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
if ([_streams count] > 0) {
|
||||
// Avoid creating an instance if we don't have it
|
||||
[self.audioStream stop];
|
||||
}
|
||||
|
||||
[_checkContentTypeRequest cancel];
|
||||
[_parsePlaylistRequest cancel];
|
||||
[_parseRssPodcastFeedRequest cancel];
|
||||
|
||||
self.readyToPlay = NO;
|
||||
}
|
||||
|
||||
- (void)pause
|
||||
{
|
||||
[self.audioStream pause];
|
||||
}
|
||||
|
||||
-(BOOL)hasMultiplePlaylistItems
|
||||
{
|
||||
return ([self.playlistItems count] > 1);
|
||||
}
|
||||
|
||||
-(BOOL)hasNextItem
|
||||
{
|
||||
return [self hasMultiplePlaylistItems] && (self.currentPlaylistItemIndex + 1 < [self.playlistItems count]);
|
||||
}
|
||||
|
||||
-(BOOL)hasPreviousItem
|
||||
{
|
||||
return ([self hasMultiplePlaylistItems] && (self.currentPlaylistItemIndex != 0));
|
||||
}
|
||||
|
||||
-(void)playNextItem
|
||||
{
|
||||
if ([self hasNextItem]) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] playNexItem. Stopping stream %@", __LINE__, self.audioStream.url);
|
||||
}
|
||||
[self.audioStream stop];
|
||||
|
||||
[self deactivateInactivateStreams:self.currentPlaylistItemIndex];
|
||||
|
||||
self.currentPlaylistItemIndex = self.currentPlaylistItemIndex + 1;
|
||||
|
||||
[self play];
|
||||
}
|
||||
}
|
||||
|
||||
-(void)playPreviousItem
|
||||
{
|
||||
if ([self hasPreviousItem]) {
|
||||
if (self.enableDebugOutput) {
|
||||
NSLog(@"[FSAudioController.m:%i] playPreviousItem. Stopping stream %@", __LINE__, self.audioStream.url);
|
||||
}
|
||||
[self.audioStream stop];
|
||||
|
||||
[self deactivateInactivateStreams:self.currentPlaylistItemIndex];
|
||||
|
||||
self.currentPlaylistItemIndex = self.currentPlaylistItemIndex - 1;
|
||||
|
||||
[self play];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Properties
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (void)setVolume:(float)volume
|
||||
{
|
||||
self.outputVolume = volume;
|
||||
self.needToSetVolume = YES;
|
||||
|
||||
if ([_streams count] > 0) {
|
||||
self.audioStream.volume = self.outputVolume;
|
||||
}
|
||||
}
|
||||
|
||||
- (float)volume
|
||||
{
|
||||
return self.outputVolume;
|
||||
}
|
||||
|
||||
- (void)setUrl:(NSURL *)url
|
||||
{
|
||||
[self stop];
|
||||
|
||||
if (url) {
|
||||
NSURL *copyOfURL = [url copy];
|
||||
_url = copyOfURL;
|
||||
|
||||
self.checkContentTypeRequest.url = _url;
|
||||
self.parsePlaylistRequest.url = _url;
|
||||
self.parseRssPodcastFeedRequest.url = _url;
|
||||
|
||||
if ([_url isFileURL]) {
|
||||
/*
|
||||
* Local file URLs can be directly played
|
||||
*/
|
||||
self.readyToPlay = YES;
|
||||
}
|
||||
} else {
|
||||
_url = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSURL* )url
|
||||
{
|
||||
if (!_url) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSURL *copyOfURL = [_url copy];
|
||||
return copyOfURL;
|
||||
}
|
||||
|
||||
- (FSAudioStream *)activeStream
|
||||
{
|
||||
if ([_streams count] > 0) {
|
||||
return self.audioStream;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (FSPlaylistItem *)currentPlaylistItem
|
||||
{
|
||||
if (self.readyToPlay) {
|
||||
if ([self.playlistItems count] > 0) {
|
||||
FSPlaylistItem *playlistItem = (self.playlistItems)[self.currentPlaylistItemIndex];
|
||||
return playlistItem;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void (^)(FSAudioStreamState state))onStateChange
|
||||
{
|
||||
return _onStateChangeBlock;
|
||||
}
|
||||
|
||||
- (void (^)(NSDictionary *metaData))onMetaDataAvailable
|
||||
{
|
||||
return _onMetaDataAvailableBlock;
|
||||
}
|
||||
|
||||
- (void (^)(FSAudioStreamError error, NSString *errorDescription))onFailure
|
||||
{
|
||||
return _onFailureBlock;
|
||||
}
|
||||
|
||||
- (void)setOnStateChange:(void (^)(FSAudioStreamState))newOnStateValue
|
||||
{
|
||||
_onStateChangeBlock = newOnStateValue;
|
||||
|
||||
if ([_streams count] > 0) {
|
||||
self.audioStream.onStateChange = _onStateChangeBlock;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setOnMetaDataAvailable:(void (^)(NSDictionary *))newOnMetaDataAvailableValue
|
||||
{
|
||||
_onMetaDataAvailableBlock = newOnMetaDataAvailableValue;
|
||||
|
||||
if ([_streams count] > 0) {
|
||||
self.audioStream.onMetaDataAvailable = _onMetaDataAvailableBlock;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setOnFailure:(void (^)(FSAudioStreamError error, NSString *errorDescription))newOnFailureValue
|
||||
{
|
||||
_onFailureBlock = newOnFailureValue;
|
||||
|
||||
if ([_streams count] > 0) {
|
||||
self.audioStream.onFailure = _onFailureBlock;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,600 +0,0 @@
|
||||
/*
|
||||
* 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 <Foundation/Foundation.h>
|
||||
#import <CoreAudio/CoreAudioTypes.h>
|
||||
|
||||
/**
|
||||
* The major version of the current release.
|
||||
*/
|
||||
#define FREESTREAMER_VERSION_MAJOR 4
|
||||
|
||||
/**
|
||||
* The minor version of the current release.
|
||||
*/
|
||||
#define FREESTREAMER_VERSION_MINOR 0
|
||||
|
||||
/**
|
||||
* The reversion of the current release
|
||||
*/
|
||||
#define FREESTREAMER_VERSION_REVISION 0
|
||||
|
||||
/**
|
||||
* Follow this notification for the audio stream state changes.
|
||||
*/
|
||||
extern NSString* const FSAudioStreamStateChangeNotification;
|
||||
extern NSString* const FSAudioStreamNotificationKey_State;
|
||||
|
||||
/**
|
||||
* Follow this notification for the audio stream errors.
|
||||
*/
|
||||
extern NSString* const FSAudioStreamErrorNotification;
|
||||
extern NSString* const FSAudioStreamNotificationKey_Error;
|
||||
|
||||
/**
|
||||
* Follow this notification for the audio stream metadata.
|
||||
*/
|
||||
extern NSString* const FSAudioStreamMetaDataNotification;
|
||||
extern NSString* const FSAudioStreamNotificationKey_MetaData;
|
||||
|
||||
/**
|
||||
* The audio stream state.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, FSAudioStreamState) {
|
||||
/**
|
||||
* Retrieving URL.
|
||||
*/
|
||||
kFsAudioStreamRetrievingURL,
|
||||
/**
|
||||
* Stopped.
|
||||
*/
|
||||
kFsAudioStreamStopped,
|
||||
/**
|
||||
* Buffering.
|
||||
*/
|
||||
kFsAudioStreamBuffering,
|
||||
/**
|
||||
* Playing.
|
||||
*/
|
||||
kFsAudioStreamPlaying,
|
||||
/**
|
||||
* Paused.
|
||||
*/
|
||||
kFsAudioStreamPaused,
|
||||
/**
|
||||
* Seeking.
|
||||
*/
|
||||
kFsAudioStreamSeeking,
|
||||
/**
|
||||
* The stream has received all the data for a file.
|
||||
*/
|
||||
kFSAudioStreamEndOfFile,
|
||||
/**
|
||||
* Failed.
|
||||
*/
|
||||
kFsAudioStreamFailed,
|
||||
/**
|
||||
* Started retrying.
|
||||
*/
|
||||
kFsAudioStreamRetryingStarted,
|
||||
/**
|
||||
* Retrying succeeded.
|
||||
*/
|
||||
kFsAudioStreamRetryingSucceeded,
|
||||
/**
|
||||
* Retrying failed.
|
||||
*/
|
||||
kFsAudioStreamRetryingFailed,
|
||||
/**
|
||||
* Playback completed.
|
||||
*/
|
||||
kFsAudioStreamPlaybackCompleted,
|
||||
/**
|
||||
* Unknown state.
|
||||
*/
|
||||
kFsAudioStreamUnknownState
|
||||
};
|
||||
|
||||
/**
|
||||
* The audio stream errors.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, FSAudioStreamError) {
|
||||
/**
|
||||
* No error.
|
||||
*/
|
||||
kFsAudioStreamErrorNone = 0,
|
||||
/**
|
||||
* Error opening the stream.
|
||||
*/
|
||||
kFsAudioStreamErrorOpen = 1,
|
||||
/**
|
||||
* Error parsing the stream.
|
||||
*/
|
||||
kFsAudioStreamErrorStreamParse = 2,
|
||||
/**
|
||||
* Network error.
|
||||
*/
|
||||
kFsAudioStreamErrorNetwork = 3,
|
||||
/**
|
||||
* Unsupported format.
|
||||
*/
|
||||
kFsAudioStreamErrorUnsupportedFormat = 4,
|
||||
/**
|
||||
* Stream buffered too often.
|
||||
*/
|
||||
kFsAudioStreamErrorStreamBouncing = 5,
|
||||
/**
|
||||
* Stream playback was terminated by the operating system.
|
||||
*/
|
||||
kFsAudioStreamErrorTerminated = 6
|
||||
};
|
||||
|
||||
@protocol FSPCMAudioStreamDelegate;
|
||||
@class FSAudioStreamPrivate;
|
||||
|
||||
/**
|
||||
* The audio stream playback position.
|
||||
*/
|
||||
typedef struct {
|
||||
unsigned minute;
|
||||
unsigned second;
|
||||
|
||||
/**
|
||||
* Playback time in seconds.
|
||||
*/
|
||||
float playbackTimeInSeconds;
|
||||
|
||||
/**
|
||||
* Position within the stream, where 0 is the beginning
|
||||
* and 1.0 is the end.
|
||||
*/
|
||||
float position;
|
||||
} FSStreamPosition;
|
||||
|
||||
/**
|
||||
* The audio stream seek byte offset.
|
||||
*/
|
||||
typedef struct {
|
||||
UInt64 start;
|
||||
UInt64 end;
|
||||
/**
|
||||
* Position within the stream, where 0 is the beginning
|
||||
* and 1.0 is the end.
|
||||
*/
|
||||
float position;
|
||||
} FSSeekByteOffset;
|
||||
|
||||
/**
|
||||
* Audio levels.
|
||||
*/
|
||||
typedef struct {
|
||||
Float32 averagePower;
|
||||
Float32 peakPower;
|
||||
} FSLevelMeterState;
|
||||
|
||||
/**
|
||||
* The low-level stream configuration.
|
||||
*/
|
||||
@interface FSStreamConfiguration : NSObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of buffers.
|
||||
*/
|
||||
@property (nonatomic,assign) unsigned bufferCount;
|
||||
/**
|
||||
* The size of each buffer.
|
||||
*/
|
||||
@property (nonatomic,assign) unsigned bufferSize;
|
||||
/**
|
||||
* The number of packet descriptions.
|
||||
*/
|
||||
@property (nonatomic,assign) unsigned maxPacketDescs;
|
||||
/**
|
||||
* The HTTP connection buffer size.
|
||||
*/
|
||||
@property (nonatomic,assign) unsigned httpConnectionBufferSize;
|
||||
/**
|
||||
* The output sample rate.
|
||||
*/
|
||||
@property (nonatomic,assign) double outputSampleRate;
|
||||
/**
|
||||
* The number of output channels.
|
||||
*/
|
||||
@property (nonatomic,assign) long outputNumChannels;
|
||||
/**
|
||||
* The interval within the stream may enter to the buffering state before it fails.
|
||||
*/
|
||||
@property (nonatomic,assign) int bounceInterval;
|
||||
/**
|
||||
* The number of times the stream may enter the buffering state before it fails.
|
||||
*/
|
||||
@property (nonatomic,assign) int maxBounceCount;
|
||||
/**
|
||||
* The stream must start within this seconds before it fails.
|
||||
*/
|
||||
@property (nonatomic,assign) int startupWatchdogPeriod;
|
||||
/**
|
||||
* Allow buffering of this many bytes before the cache is full.
|
||||
*/
|
||||
@property (nonatomic,assign) int maxPrebufferedByteCount;
|
||||
/**
|
||||
* Calculate prebuffer sizes dynamically using the stream bitrate in seconds instead of bytes.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL usePrebufferSizeCalculationInSeconds;
|
||||
/**
|
||||
* Calculate prebuffer sizes using the packet counts.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL usePrebufferSizeCalculationInPackets;
|
||||
/**
|
||||
* Require buffering of this many bytes before the playback can start for a continuous stream.
|
||||
*/
|
||||
@property (nonatomic,assign) float requiredPrebufferSizeInSeconds;
|
||||
/**
|
||||
* Require buffering of this many bytes before the playback can start for a continuous stream.
|
||||
*/
|
||||
@property (nonatomic,assign) int requiredInitialPrebufferedByteCountForContinuousStream;
|
||||
/**
|
||||
* Require buffering of this many bytes before the playback can start a non-continuous stream.
|
||||
*/
|
||||
@property (nonatomic,assign) int requiredInitialPrebufferedByteCountForNonContinuousStream;
|
||||
/**
|
||||
* Require buffering of this many packets before the playback can start.
|
||||
*/
|
||||
@property (nonatomic,assign) int requiredInitialPrebufferedPacketCount;
|
||||
/**
|
||||
* The HTTP user agent used for stream operations.
|
||||
*/
|
||||
@property (nonatomic,strong) NSString *userAgent;
|
||||
/**
|
||||
* The directory used for caching the streamed files.
|
||||
*/
|
||||
@property (nonatomic,strong) NSString *cacheDirectory;
|
||||
/**
|
||||
* The HTTP headers that are appended to the request when the streaming starts. Notice
|
||||
* that the headers override any headers previously set by FreeStreamer.
|
||||
*/
|
||||
@property (nonatomic,strong) NSDictionary *predefinedHttpHeaderValues;
|
||||
/**
|
||||
* The property determining if caching the streams to the disk is enabled.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL cacheEnabled;
|
||||
/**
|
||||
* The property determining if seeking from the audio packets stored in cache is enabled.
|
||||
* The benefit is that seeking is faster in the case the audio packets are already cached in memory.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL seekingFromCacheEnabled;
|
||||
/**
|
||||
* The property determining if FreeStreamer should handle audio session automatically.
|
||||
* Leave it on if you don't want to handle the audio session by yourself.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL automaticAudioSessionHandlingEnabled;
|
||||
/**
|
||||
* The property enables time and pitch conversion for the audio queue. Put it on
|
||||
* if you want to use the play rate setting.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL enableTimeAndPitchConversion;
|
||||
/**
|
||||
* Requires the content type given by the server to match an audio content type.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL requireStrictContentTypeChecking;
|
||||
/**
|
||||
* The maximum size of the disk cache in bytes.
|
||||
*/
|
||||
@property (nonatomic,assign) int maxDiskCacheSize;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Statistics on the stream state.
|
||||
*/
|
||||
@interface FSStreamStatistics : NSObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when the statistics were gathered.
|
||||
*/
|
||||
@property (nonatomic,strong) NSDate *snapshotTime;
|
||||
/**
|
||||
* Time in a pretty format.
|
||||
*/
|
||||
@property (nonatomic,readonly) NSString *snapshotTimeFormatted;
|
||||
/**
|
||||
* Audio stream packet count.
|
||||
*/
|
||||
@property (nonatomic,assign) NSUInteger audioStreamPacketCount;
|
||||
/**
|
||||
* Audio queue used buffers count.
|
||||
*/
|
||||
@property (nonatomic,assign) NSUInteger audioQueueUsedBufferCount;
|
||||
/**
|
||||
* Audio stream PCM packet queue count.
|
||||
*/
|
||||
@property (nonatomic,assign) NSUInteger audioQueuePCMPacketQueueCount;
|
||||
|
||||
@end
|
||||
|
||||
NSString* freeStreamerReleaseVersion(void);
|
||||
|
||||
/**
|
||||
* FSAudioStream is a class for streaming audio files from an URL.
|
||||
* It must be directly fed with an URL, which contains audio. That is,
|
||||
* playlists or other non-audio formats yield an error.
|
||||
*
|
||||
* To start playback, the stream must be either initialized with an URL
|
||||
* or the playback URL can be set with the url property. The playback
|
||||
* is started with the play method. It is possible to pause or stop
|
||||
* the stream with the respective methods.
|
||||
*
|
||||
* Non-continuous streams (audio streams with a known duration) can be
|
||||
* seeked with the seekToPosition method.
|
||||
*
|
||||
* Note that FSAudioStream is not designed to be thread-safe! That means
|
||||
* that using the streamer from multiple threads without syncronization
|
||||
* could cause problems. It is recommended to keep the streamer in the
|
||||
* main thread and call the streamer methods only from the main thread
|
||||
* (consider using performSelectorOnMainThread: if calls from multiple
|
||||
* threads are needed).
|
||||
*/
|
||||
@interface FSAudioStream : NSObject {
|
||||
FSAudioStreamPrivate *_private;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the audio stream with an URL.
|
||||
*
|
||||
* @param url The URL from which the stream data is retrieved.
|
||||
*/
|
||||
- (id)initWithUrl:(NSURL *)url;
|
||||
|
||||
/**
|
||||
* Initializes the stream with a configuration.
|
||||
*
|
||||
* @param configuration The stream configuration.
|
||||
*/
|
||||
- (id)initWithConfiguration:(FSStreamConfiguration *)configuration;
|
||||
|
||||
/**
|
||||
* Starts preload the stream. If no preload URL is
|
||||
* defined, an error will occur.
|
||||
*/
|
||||
- (void)preload;
|
||||
|
||||
/**
|
||||
* Starts playing the stream. If no playback URL is
|
||||
* defined, an error will occur.
|
||||
*/
|
||||
- (void)play;
|
||||
|
||||
/**
|
||||
* Starts playing the stream from the given URL.
|
||||
*
|
||||
* @param url The URL from which the stream data is retrieved.
|
||||
*/
|
||||
- (void)playFromURL:(NSURL*)url;
|
||||
|
||||
/**
|
||||
* Starts playing the stream from the given offset.
|
||||
* The offset can be retrieved from the stream with the
|
||||
* currentSeekByteOffset property.
|
||||
*
|
||||
* @param offset The offset where to start playback from.
|
||||
*/
|
||||
- (void)playFromOffset:(FSSeekByteOffset)offset;
|
||||
|
||||
/**
|
||||
* Stops the stream playback.
|
||||
*/
|
||||
- (void)stop;
|
||||
|
||||
/**
|
||||
* If the stream is playing, the stream playback is paused upon calling pause.
|
||||
* Otherwise (the stream is paused), calling pause will continue the playback.
|
||||
*/
|
||||
- (void)pause;
|
||||
|
||||
/**
|
||||
* Rewinds the stream. Only possible for continuous streams.
|
||||
*
|
||||
* @param seconds Seconds to rewind the stream.
|
||||
*/
|
||||
- (void)rewind:(unsigned)seconds;
|
||||
|
||||
/**
|
||||
* Seeks the stream to a given position. Requires a non-continuous stream
|
||||
* (a stream with a known duration).
|
||||
*
|
||||
* @param position The stream position to seek to.
|
||||
*/
|
||||
- (void)seekToPosition:(FSStreamPosition)position;
|
||||
|
||||
/**
|
||||
* Sets the audio stream playback rate from 0.5 to 2.0.
|
||||
* Value 1.0 means the normal playback rate. Values below
|
||||
* 1.0 means a slower playback rate than usual and above
|
||||
* 1.0 a faster playback rate. Notice that using a faster
|
||||
* playback rate than 1.0 may mean that you have to increase
|
||||
* the buffer sizes for the stream still to play.
|
||||
*
|
||||
* The play rate has only effect if the stream is playing.
|
||||
*
|
||||
* @param playRate The playback rate.
|
||||
*/
|
||||
- (void)setPlayRate:(float)playRate;
|
||||
|
||||
/**
|
||||
* Returns the playback status: YES if the stream is playing, NO otherwise.
|
||||
*/
|
||||
- (BOOL)isPlaying;
|
||||
|
||||
/**
|
||||
* Cleans all cached data from the persistent storage.
|
||||
*/
|
||||
- (void)expungeCache;
|
||||
|
||||
/**
|
||||
* The stream URL.
|
||||
*/
|
||||
@property (nonatomic,assign) NSURL *url;
|
||||
/**
|
||||
* Determines if strict content type checking is required. If the audio stream
|
||||
* cannot determine that the stream is actually an audio stream, the stream
|
||||
* does not play. Disabling strict content type checking bypasses the
|
||||
* stream content type checks and tries to play the stream regardless
|
||||
* of the content type information given by the server.
|
||||
*/
|
||||
@property (nonatomic,assign) BOOL strictContentTypeChecking;
|
||||
/**
|
||||
* Set an output file to store the stream contents to a file.
|
||||
*/
|
||||
@property (nonatomic,assign) NSURL *outputFile;
|
||||
/**
|
||||
* Sets a default content type for the stream. Used if
|
||||
* the stream content type is not available.
|
||||
*/
|
||||
@property (nonatomic,assign) NSString *defaultContentType;
|
||||
/**
|
||||
* The property has the content type of the stream, for instance audio/mpeg.
|
||||
*/
|
||||
@property (nonatomic,readonly) NSString *contentType;
|
||||
/**
|
||||
* The property has the suggested file extension for the stream based on the stream content type.
|
||||
*/
|
||||
@property (nonatomic,readonly) NSString *suggestedFileExtension;
|
||||
/**
|
||||
* Sets a default content length for the stream. Used if
|
||||
* the stream content-length is not available.
|
||||
*/
|
||||
@property (nonatomic, assign) UInt64 defaultContentLength;
|
||||
/**
|
||||
* The property has the content length of the stream (in bytes). The length is zero if
|
||||
* the stream is continuous.
|
||||
*/
|
||||
@property (nonatomic,readonly) UInt64 contentLength;
|
||||
/**
|
||||
* The number of bytes of audio data. Notice that this may differ
|
||||
* from the number of bytes the server returns for the content length!
|
||||
* For instance audio file meta data is excluded from the count.
|
||||
* Effectively you can use this property for seeking calculations.
|
||||
*/
|
||||
@property (nonatomic,readonly) UInt64 audioDataByteCount;
|
||||
/**
|
||||
* This property has the current playback position, if the stream is non-continuous.
|
||||
* The current playback position cannot be determined for continuous streams.
|
||||
*/
|
||||
@property (nonatomic,readonly) FSStreamPosition currentTimePlayed;
|
||||
/**
|
||||
* This property has the duration of the stream, if the stream is non-continuous.
|
||||
* Continuous streams do not have a duration.
|
||||
*/
|
||||
@property (nonatomic,readonly) FSStreamPosition duration;
|
||||
/**
|
||||
* This property has the current seek byte offset of the stream, if the stream is non-continuous.
|
||||
* Continuous streams do not have a seek byte offset.
|
||||
*/
|
||||
@property (nonatomic,readonly) FSSeekByteOffset currentSeekByteOffset;
|
||||
/**
|
||||
* This property has the bit rate of the stream. The bit rate is initially 0,
|
||||
* before the stream has processed enough packets to calculate the bit rate.
|
||||
*/
|
||||
@property (nonatomic,readonly) float bitRate;
|
||||
/**
|
||||
* The property is true if the stream is continuous (no known duration).
|
||||
*/
|
||||
@property (nonatomic,readonly) BOOL continuous;
|
||||
/**
|
||||
* The property is true if the stream has been cached locally.
|
||||
*/
|
||||
@property (nonatomic,readonly) BOOL cached;
|
||||
/**
|
||||
* This property has the number of bytes buffered for this stream.
|
||||
*/
|
||||
@property (nonatomic,readonly) size_t prebufferedByteCount;
|
||||
/**
|
||||
* This property holds the current playback volume of the stream,
|
||||
* from 0.0 to 1.0.
|
||||
*
|
||||
* Note that the overall volume is still constrained by the volume
|
||||
* set by the user! So the actual volume cannot be higher
|
||||
* than the volume currently set by the user. For example, if
|
||||
* requesting a volume of 0.5, then the volume will be 50%
|
||||
* lower than the current playback volume set by the user.
|
||||
*/
|
||||
@property (nonatomic,assign) float volume;
|
||||
/**
|
||||
* The current size of the disk cache.
|
||||
*/
|
||||
@property (nonatomic,readonly) unsigned long long totalCachedObjectsSize;
|
||||
/**
|
||||
* The property determines the amount of times the stream has tried to retry the playback
|
||||
* in case of failure.
|
||||
*/
|
||||
@property (nonatomic,readonly) NSUInteger retryCount;
|
||||
/**
|
||||
* Holds the maximum amount of playback retries that will be
|
||||
* performed before entering kFsAudioStreamRetryingFailed state.
|
||||
* Default is 3.
|
||||
*/
|
||||
@property (nonatomic,assign) NSUInteger maxRetryCount;
|
||||
/**
|
||||
* The property determines the current audio levels.
|
||||
*/
|
||||
@property (nonatomic,readonly) FSLevelMeterState levels;
|
||||
/**
|
||||
* This property holds the current statistics for the stream state.
|
||||
*/
|
||||
@property (nonatomic,readonly) FSStreamStatistics *statistics;
|
||||
/**
|
||||
* Called upon completion of the stream. Note that for continuous
|
||||
* streams this is never called.
|
||||
*/
|
||||
@property (copy) void (^onCompletion)(void);
|
||||
/**
|
||||
* Called upon a state change.
|
||||
*/
|
||||
@property (copy) void (^onStateChange)(FSAudioStreamState state);
|
||||
/**
|
||||
* Called upon a meta data is available.
|
||||
*/
|
||||
@property (copy) void (^onMetaDataAvailable)(NSDictionary *metadata);
|
||||
/**
|
||||
* Called upon a failure.
|
||||
*/
|
||||
@property (copy) void (^onFailure)(FSAudioStreamError error, NSString *errorDescription);
|
||||
/**
|
||||
* The property has the low-level stream configuration.
|
||||
*/
|
||||
@property (readonly) FSStreamConfiguration *configuration;
|
||||
/**
|
||||
* Delegate.
|
||||
*/
|
||||
@property (nonatomic,unsafe_unretained) IBOutlet id<FSPCMAudioStreamDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* To access the PCM audio data, use this delegate.
|
||||
*/
|
||||
@protocol FSPCMAudioStreamDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
/**
|
||||
* Called when there are PCM audio samples available. Do not do any blocking operations
|
||||
* when you receive the data. Instead, copy the data and process it so that the
|
||||
* main event loop doesn't block. Failing to do so may cause glitches to the audio playback.
|
||||
*
|
||||
* Notice that the delegate callback may occur from other than the main thread so make
|
||||
* sure your delegate code is thread safe.
|
||||
*
|
||||
* @param audioStream The audio stream the samples are from.
|
||||
* @param samples The samples as a buffer list.
|
||||
* @param frames The number of frames.
|
||||
* @param description Description of the data provided.
|
||||
*/
|
||||
- (void)audioStream:(FSAudioStream *)audioStream samplesAvailable:(AudioBufferList *)samples frames:(UInt32)frames description: (AudioStreamPacketDescription)description;
|
||||
@end
|
||||
1905
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSAudioStream.mm
generated
1905
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSAudioStream.mm
generated
File diff suppressed because it is too large
Load Diff
@ -1,128 +0,0 @@
|
||||
/*
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* Content type format.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, FSFileFormat) {
|
||||
/**
|
||||
* Unknown format.
|
||||
*/
|
||||
kFSFileFormatUnknown = 0,
|
||||
|
||||
/**
|
||||
* M3U playlist.
|
||||
*/
|
||||
kFSFileFormatM3UPlaylist,
|
||||
/**
|
||||
* PLS playlist.
|
||||
*/
|
||||
kFSFileFormatPLSPlaylist,
|
||||
|
||||
/**
|
||||
* XML file.
|
||||
*/
|
||||
kFSFileFormatXML,
|
||||
|
||||
/**
|
||||
* MP3 file.
|
||||
*/
|
||||
kFSFileFormatMP3,
|
||||
/**
|
||||
* WAVE file.
|
||||
*/
|
||||
kFSFileFormatWAVE,
|
||||
/**
|
||||
* AIFC file.
|
||||
*/
|
||||
kFSFileFormatAIFC,
|
||||
/**
|
||||
* AIFF file.
|
||||
*/
|
||||
kFSFileFormatAIFF,
|
||||
/**
|
||||
* M4A file.
|
||||
*/
|
||||
kFSFileFormatM4A,
|
||||
/**
|
||||
* MPEG4 file.
|
||||
*/
|
||||
kFSFileFormatMPEG4,
|
||||
/**
|
||||
* CAF file.
|
||||
*/
|
||||
kFSFileFormatCAF,
|
||||
/**
|
||||
* AAC_ADTS file.
|
||||
*/
|
||||
kFSFileFormatAAC_ADTS,
|
||||
|
||||
/**
|
||||
* Total number of formats.
|
||||
*/
|
||||
kFSFileFormatCount
|
||||
};
|
||||
|
||||
/**
|
||||
* FSCheckContentTypeRequest is a class for checking the content type
|
||||
* of a URL. It makes an HTTP HEAD request and parses the header information
|
||||
* from the server. The resulting format is stored in the format property.
|
||||
*
|
||||
* To use the class, define the URL for checking the content type using
|
||||
* the url property. Then, define the onCompletion and onFailure handlers.
|
||||
* To start the request, use the start method.
|
||||
*/
|
||||
@interface FSCheckContentTypeRequest : NSObject <NSURLSessionDelegate> {
|
||||
NSURLSessionTask *_task;
|
||||
FSFileFormat _format;
|
||||
NSString *_contentType;
|
||||
BOOL _playlist;
|
||||
BOOL _xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL of this request.
|
||||
*/
|
||||
@property (nonatomic,copy) NSURL *url;
|
||||
/**
|
||||
* Called when the content type determination is completed.
|
||||
*/
|
||||
@property (copy) void (^onCompletion)(void);
|
||||
/**
|
||||
* Called if the content type determination failed.
|
||||
*/
|
||||
@property (copy) void (^onFailure)(void);
|
||||
/**
|
||||
* Contains the format of the URL upon completion of the request.
|
||||
*/
|
||||
@property (nonatomic,readonly) FSFileFormat format;
|
||||
/**
|
||||
* Containts the content type of the URL upon completion of the request.
|
||||
*/
|
||||
@property (nonatomic,readonly) NSString *contentType;
|
||||
/**
|
||||
* The property is true if the URL contains a playlist.
|
||||
*/
|
||||
@property (nonatomic,readonly) BOOL playlist;
|
||||
/**
|
||||
* The property is true if the URL contains XML data.
|
||||
*/
|
||||
@property (nonatomic,readonly) BOOL xml;
|
||||
|
||||
/**
|
||||
* Starts the request.
|
||||
*/
|
||||
- (void)start;
|
||||
/**
|
||||
* Cancels the request.
|
||||
*/
|
||||
- (void)cancel;
|
||||
|
||||
@end
|
||||
@ -1,236 +0,0 @@
|
||||
/*
|
||||
* 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 "FSCheckContentTypeRequest.h"
|
||||
|
||||
@interface FSCheckContentTypeRequest ()
|
||||
- (BOOL)guessContentTypeByUrl:(NSURLResponse *)response;
|
||||
@end
|
||||
|
||||
@implementation FSCheckContentTypeRequest
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_format = kFSFileFormatUnknown;
|
||||
_playlist = NO;
|
||||
_xml = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (_task) {
|
||||
return;
|
||||
}
|
||||
|
||||
_format = kFSFileFormatUnknown;
|
||||
_playlist = NO;
|
||||
_contentType = @"";
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url
|
||||
cachePolicy:NSURLRequestReloadIgnoringCacheData
|
||||
timeoutInterval:10.0];
|
||||
[request setHTTPMethod:@"HEAD"];
|
||||
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
|
||||
delegate:self
|
||||
delegateQueue:[NSOperationQueue mainQueue]];
|
||||
|
||||
@synchronized (self) {
|
||||
_task = [session dataTaskWithRequest:request];
|
||||
}
|
||||
[_task resume];
|
||||
|
||||
if (!_task) {
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSCheckContentTypeRequest: Unable to open connection for URL: %@", _url);
|
||||
#endif
|
||||
|
||||
self.onFailure();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
if (!_task) {
|
||||
return;
|
||||
}
|
||||
@synchronized (self) {
|
||||
[_task cancel];
|
||||
_task = nil;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Properties
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (FSFileFormat)format
|
||||
{
|
||||
return _format;
|
||||
}
|
||||
|
||||
- (NSString *)contentType
|
||||
{
|
||||
return _contentType;
|
||||
}
|
||||
|
||||
- (BOOL)playlist
|
||||
{
|
||||
return _playlist;
|
||||
}
|
||||
|
||||
- (BOOL)xml
|
||||
{
|
||||
return _xml;
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* NSURLSessionDelegate
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveResponse:(NSURLResponse *)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
|
||||
|
||||
_contentType = response.MIMEType;
|
||||
|
||||
_format = kFSFileFormatUnknown;
|
||||
_playlist = NO;
|
||||
|
||||
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
|
||||
|
||||
if (statusCode >= 200 && statusCode <= 299) {
|
||||
// Only use the content type if the response indicated success (2xx)
|
||||
|
||||
if ([_contentType isEqualToString:@"audio/mpeg"]) {
|
||||
_format = kFSFileFormatMP3;
|
||||
} else if ([_contentType isEqualToString:@"audio/x-wav"]) {
|
||||
_format = kFSFileFormatWAVE;
|
||||
} else if ([_contentType isEqualToString:@"audio/x-aifc"]) {
|
||||
_format = kFSFileFormatAIFC;
|
||||
} else if ([_contentType isEqualToString:@"audio/x-aiff"]) {
|
||||
_format = kFSFileFormatAIFF;
|
||||
} else if ([_contentType isEqualToString:@"audio/x-m4a"]) {
|
||||
_format = kFSFileFormatM4A;
|
||||
} else if ([_contentType isEqualToString:@"audio/mp4"]) {
|
||||
_format = kFSFileFormatMPEG4;
|
||||
} else if ([_contentType isEqualToString:@"audio/x-caf"]) {
|
||||
_format = kFSFileFormatCAF;
|
||||
} else if ([_contentType isEqualToString:@"audio/aac"] ||
|
||||
[_contentType isEqualToString:@"audio/aacp"]) {
|
||||
_format = kFSFileFormatAAC_ADTS;
|
||||
} else if ([_contentType isEqualToString:@"audio/x-mpegurl"] ||
|
||||
[_contentType isEqualToString:@"application/x-mpegurl"]) {
|
||||
_format = kFSFileFormatM3UPlaylist;
|
||||
_playlist = YES;
|
||||
} else if ([_contentType isEqualToString:@"audio/x-scpls"] ||
|
||||
[_contentType isEqualToString:@"application/pls+xml"]) {
|
||||
_format = kFSFileFormatPLSPlaylist;
|
||||
_playlist = YES;
|
||||
} else if ([_contentType isEqualToString:@"text/xml"] ||
|
||||
[_contentType isEqualToString:@"application/xml"]) {
|
||||
_format = kFSFileFormatXML;
|
||||
_xml = YES;
|
||||
} else {
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSCheckContentTypeRequest: Cannot resolve %@, guessing the content type by URL: %@", _contentType, _url);
|
||||
#endif
|
||||
[self guessContentTypeByUrl:response];
|
||||
}
|
||||
} else {
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSCheckContentTypeRequest: Invalid HTTP status code received %li, guessing the content type by URL: %@", (long)statusCode, _url);
|
||||
#endif
|
||||
[self guessContentTypeByUrl:response];
|
||||
}
|
||||
|
||||
_task = nil;
|
||||
|
||||
self.onCompletion();
|
||||
}
|
||||
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(nullable NSError *)error {
|
||||
|
||||
@synchronized (self) {
|
||||
_task = nil;
|
||||
_format = kFSFileFormatUnknown;
|
||||
_playlist = NO;
|
||||
}
|
||||
|
||||
// Still, try if we could resolve the content type by the URL
|
||||
if ([self guessContentTypeByUrl:nil]) {
|
||||
self.onCompletion();
|
||||
} else {
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSCheckContentTypeRequest: Unable to determine content-type for the URL: %@, error %@", _url, [error localizedDescription]);
|
||||
#endif
|
||||
|
||||
self.onFailure();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Private
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (BOOL)guessContentTypeByUrl:(NSURLResponse *)response
|
||||
{
|
||||
/* The server did not provide meaningful content type;
|
||||
last resort: check the file suffix, if there is one */
|
||||
|
||||
NSString *absoluteUrl;
|
||||
|
||||
if (response) {
|
||||
absoluteUrl = [response.URL absoluteString];
|
||||
} else {
|
||||
absoluteUrl = [_url absoluteString];
|
||||
}
|
||||
|
||||
if ([absoluteUrl hasSuffix:@".mp3"]) {
|
||||
_format = kFSFileFormatMP3;
|
||||
} else if ([absoluteUrl hasSuffix:@".mp4"]) {
|
||||
_format = kFSFileFormatMPEG4;
|
||||
} else if ([absoluteUrl hasSuffix:@".m3u"]) {
|
||||
_format = kFSFileFormatM3UPlaylist;
|
||||
_playlist = YES;
|
||||
} else if ([absoluteUrl hasSuffix:@".pls"]) {
|
||||
_format = kFSFileFormatPLSPlaylist;
|
||||
_playlist = YES;
|
||||
} else if ([absoluteUrl hasSuffix:@".xml"]) {
|
||||
_format = kFSFileFormatXML;
|
||||
_xml = YES;
|
||||
} else {
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSCheckContentTypeRequest: Failed to determine content type from the URL: %@", _url);
|
||||
#endif
|
||||
/*
|
||||
* Failed to guess the content type based on the URL.
|
||||
*/
|
||||
return NO;
|
||||
}
|
||||
|
||||
/*
|
||||
* We have determined a content-type.
|
||||
*/
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* The playlist format.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, FSPlaylistFormat) {
|
||||
/**
|
||||
* Unknown playlist format.
|
||||
*/
|
||||
kFSPlaylistFormatNone,
|
||||
/**
|
||||
* M3U playlist.
|
||||
*/
|
||||
kFSPlaylistFormatM3U,
|
||||
/**
|
||||
* PLS playlist.
|
||||
*/
|
||||
kFSPlaylistFormatPLS
|
||||
};
|
||||
|
||||
/**
|
||||
* FSParsePlaylistRequest is a class for parsing a playlist. It supports
|
||||
* the M3U and PLS formats.
|
||||
*
|
||||
* To use the class, define the URL for retrieving the playlist using
|
||||
* the url property. Then, define the onCompletion and onFailure handlers.
|
||||
* To start the request, use the start method.
|
||||
*/
|
||||
@interface FSParsePlaylistRequest : NSObject<NSURLSessionDelegate> {
|
||||
NSURLSessionTask *_task;
|
||||
NSInteger _httpStatus;
|
||||
NSMutableData *_receivedData;
|
||||
NSMutableArray *_playlistItems;
|
||||
FSPlaylistFormat _format;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL of this request.
|
||||
*/
|
||||
@property (nonatomic,copy) NSURL *url;
|
||||
/**
|
||||
* Called when the playlist parsing is completed.
|
||||
*/
|
||||
@property (copy) void (^onCompletion)(void);
|
||||
/**
|
||||
* Called if the playlist parsing failed.
|
||||
*/
|
||||
@property (copy) void (^onFailure)(void);
|
||||
/**
|
||||
* The playlist items stored in the FSPlaylistItem class.
|
||||
*/
|
||||
@property (readonly) NSMutableArray *playlistItems;
|
||||
|
||||
/**
|
||||
* Starts the request.
|
||||
*/
|
||||
- (void)start;
|
||||
/**
|
||||
* Cancels the request.
|
||||
*/
|
||||
- (void)cancel;
|
||||
|
||||
@end
|
||||
@ -1,325 +0,0 @@
|
||||
/*
|
||||
* 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 "FSParsePlaylistRequest.h"
|
||||
#import "FSPlaylistItem.h"
|
||||
|
||||
@interface FSParsePlaylistRequest ()
|
||||
- (void)parsePlaylistFromData:(NSData *)data;
|
||||
- (void)parsePlaylistM3U:(NSString *)playlist;
|
||||
- (void)parsePlaylistPLS:(NSString *)playlist;
|
||||
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl;
|
||||
|
||||
@property (readonly) FSPlaylistFormat format;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FSParsePlaylistRequest
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (_task) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:self.url
|
||||
cachePolicy:NSURLRequestUseProtocolCachePolicy
|
||||
timeoutInterval:10.0];
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
|
||||
delegate:self
|
||||
delegateQueue:[NSOperationQueue mainQueue]];
|
||||
|
||||
@synchronized (self) {
|
||||
_receivedData = [NSMutableData data];
|
||||
_task = [session dataTaskWithRequest:request];
|
||||
_playlistItems = [[NSMutableArray alloc] init];
|
||||
_format = kFSPlaylistFormatNone;
|
||||
}
|
||||
|
||||
[_task resume];
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
if (!_task) {
|
||||
return;
|
||||
}
|
||||
@synchronized (self) {
|
||||
[_task cancel];
|
||||
_task = nil;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Properties
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (NSMutableArray *)playlistItems
|
||||
{
|
||||
return [_playlistItems copy];
|
||||
}
|
||||
|
||||
- (FSPlaylistFormat)format
|
||||
{
|
||||
return _format;
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Private
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (void)parsePlaylistFromData:(NSData *)data
|
||||
{
|
||||
NSString *playlistData = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
|
||||
|
||||
if (_format == kFSPlaylistFormatM3U) {
|
||||
[self parsePlaylistM3U:playlistData];
|
||||
|
||||
if ([_playlistItems count] == 0) {
|
||||
// If we failed to grab any playlist items, still try
|
||||
// to parse it in another format; perhaps the server
|
||||
// mistakingly identified the playlist format
|
||||
|
||||
[self parsePlaylistPLS:playlistData];
|
||||
}
|
||||
} else if (_format == kFSPlaylistFormatPLS) {
|
||||
[self parsePlaylistPLS:playlistData];
|
||||
|
||||
if ([_playlistItems count] == 0) {
|
||||
// If we failed to grab any playlist items, still try
|
||||
// to parse it in another format; perhaps the server
|
||||
// mistakingly identified the playlist format
|
||||
|
||||
[self parsePlaylistM3U:playlistData];
|
||||
}
|
||||
}
|
||||
|
||||
if ([_playlistItems count] == 0) {
|
||||
/*
|
||||
* Fail if we failed to parse any items from the playlist.
|
||||
*/
|
||||
self.onFailure();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)parsePlaylistM3U:(NSString *)playlist
|
||||
{
|
||||
[_playlistItems removeAllObjects];
|
||||
|
||||
for (NSString *line in [playlist componentsSeparatedByString:@"\n"]) {
|
||||
if ([line hasPrefix:@"#"]) {
|
||||
/* metadata, skip */
|
||||
continue;
|
||||
}
|
||||
if ([line hasPrefix:@"http://"] ||
|
||||
[line hasPrefix:@"https://"]) {
|
||||
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
|
||||
item.url = [NSURL URLWithString:[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
|
||||
|
||||
[_playlistItems addObject:item];
|
||||
} else if ([line hasPrefix:@"file://"]) {
|
||||
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
|
||||
item.url = [self parseLocalFileUrl:line];
|
||||
|
||||
[_playlistItems addObject:item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)parsePlaylistPLS:(NSString *)playlist
|
||||
{
|
||||
[_playlistItems removeAllObjects];
|
||||
|
||||
NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
|
||||
|
||||
size_t i = 0;
|
||||
|
||||
for (NSString *rawLine in [playlist componentsSeparatedByString:@"\n"]) {
|
||||
NSString *line = [rawLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
if (i == 0) {
|
||||
if ([[line lowercaseString] hasPrefix:@"[playlist]"]) {
|
||||
i++;
|
||||
continue;
|
||||
} else {
|
||||
// Invalid playlist; the first line should indicate that this is a playlist
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore empty lines
|
||||
if ([line length] == 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not an empty line; so expect that this is a key/value pair
|
||||
NSRange r = [line rangeOfString:@"="];
|
||||
|
||||
// Invalid format, key/value pair not found
|
||||
if (r.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *key = [[line substringToIndex:r.location] lowercaseString];
|
||||
NSString *value = [line substringFromIndex:r.location + 1];
|
||||
|
||||
props[key] = value;
|
||||
i++;
|
||||
}
|
||||
|
||||
NSInteger numItems = [[props valueForKey:@"numberofentries"] integerValue];
|
||||
|
||||
if (numItems == 0) {
|
||||
// Invalid playlist; number of playlist items not defined
|
||||
return;
|
||||
}
|
||||
|
||||
for (i=0; i < numItems; i++) {
|
||||
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
|
||||
|
||||
NSString *title = [props valueForKey:[NSString stringWithFormat:@"title%lu", (i+1)]];
|
||||
|
||||
item.title = title;
|
||||
|
||||
NSString *file = [props valueForKey:[NSString stringWithFormat:@"file%lu", (i+1)]];
|
||||
|
||||
if ([file hasPrefix:@"http://"] ||
|
||||
[file hasPrefix:@"https://"]) {
|
||||
item.url = [NSURL URLWithString:file];
|
||||
|
||||
[_playlistItems addObject:item];
|
||||
} else if ([file hasPrefix:@"file://"]) {
|
||||
item.url = [self parseLocalFileUrl:file];
|
||||
|
||||
[_playlistItems addObject:item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl
|
||||
{
|
||||
// Resolve the local bundle URL
|
||||
NSString *path = [fileUrl substringFromIndex:7];
|
||||
|
||||
NSRange range = [path rangeOfString:@"." options:NSBackwardsSearch];
|
||||
|
||||
NSString *fileName = [path substringWithRange:NSMakeRange(0, range.location)];
|
||||
NSString *suffix = [path substringWithRange:NSMakeRange(range.location + 1, [path length] - [fileName length] - 1)];
|
||||
|
||||
return [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:fileName ofType:suffix]];
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* NSURLSessionDelegate
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveResponse:(NSURLResponse *)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
|
||||
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||
_httpStatus = [httpResponse statusCode];
|
||||
|
||||
NSString *contentType = response.MIMEType;
|
||||
NSString *absoluteUrl = [response.URL absoluteString];
|
||||
|
||||
_format = kFSPlaylistFormatNone;
|
||||
|
||||
if ([contentType isEqualToString:@"audio/x-mpegurl"] ||
|
||||
[contentType isEqualToString:@"application/x-mpegurl"]) {
|
||||
_format = kFSPlaylistFormatM3U;
|
||||
} else if ([contentType isEqualToString:@"audio/x-scpls"] ||
|
||||
[contentType isEqualToString:@"application/pls+xml"]) {
|
||||
_format = kFSPlaylistFormatPLS;
|
||||
} else if ([contentType isEqualToString:@"text/plain"]) {
|
||||
/* The server did not provide meaningful content type;
|
||||
last resort: check the file suffix, if there is one */
|
||||
|
||||
if ([absoluteUrl hasSuffix:@".m3u"]) {
|
||||
_format = kFSPlaylistFormatM3U;
|
||||
} else if ([absoluteUrl hasSuffix:@".pls"]) {
|
||||
_format = kFSPlaylistFormatPLS;
|
||||
}
|
||||
}
|
||||
|
||||
if (_format == kFSPlaylistFormatNone) {
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSParsePlaylistRequest: Unable to determine the type of the playlist for URL: %@", _url);
|
||||
#endif
|
||||
|
||||
self.onFailure();
|
||||
|
||||
} else {
|
||||
completionHandler(NSURLSessionResponseAllow);
|
||||
}
|
||||
|
||||
[_receivedData setLength:0];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
|
||||
{
|
||||
// Resume the Download Task manually because apparently iOS does not do it automatically?!
|
||||
[downloadTask resume];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveData:(NSData *)data {
|
||||
[_receivedData appendData:data];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(nullable NSError *)error {
|
||||
if(error) {
|
||||
@synchronized (self) {
|
||||
_task = nil;
|
||||
_receivedData = nil;
|
||||
}
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSParsePlaylistRequest: Connection failed for URL: %@, error %@", _url, [error localizedDescription]);
|
||||
#endif
|
||||
|
||||
self.onFailure();
|
||||
} else {
|
||||
@synchronized (self) {
|
||||
_task = nil;
|
||||
}
|
||||
|
||||
if (_httpStatus != 200) {
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSParsePlaylistRequest: Unable to receive playlist from URL: %@", _url);
|
||||
#endif
|
||||
|
||||
self.onFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
[self parsePlaylistFromData:_receivedData];
|
||||
|
||||
self.onCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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 "FSXMLHttpRequest.h"
|
||||
|
||||
/**
|
||||
* Use this request for retrieving the contents for a podcast RSS feed.
|
||||
* Upon request completion, the resulting playlist items are
|
||||
* in the playlistItems property.
|
||||
*
|
||||
* See the FSXMLHttpRequest class how to form a request to retrieve
|
||||
* the RSS feed.
|
||||
*/
|
||||
@interface FSParseRssPodcastFeedRequest : FSXMLHttpRequest {
|
||||
NSMutableArray *_playlistItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* The playlist items stored in the FSPlaylistItem class.
|
||||
*/
|
||||
@property (readonly) NSMutableArray *playlistItems;
|
||||
|
||||
@end
|
||||
@ -1,100 +0,0 @@
|
||||
/*
|
||||
* 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 <libxml/parser.h>
|
||||
#import <libxml/xpath.h>
|
||||
|
||||
#import "FSParseRssPodcastFeedRequest.h"
|
||||
#import "FSPlaylistItem.h"
|
||||
|
||||
static NSString *const kXPathQueryItems = @"/rss/channel/item";
|
||||
|
||||
@interface FSParseRssPodcastFeedRequest ()
|
||||
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl;
|
||||
- (void)parseItems:(xmlNodePtr)node;
|
||||
@end
|
||||
|
||||
@implementation FSParseRssPodcastFeedRequest
|
||||
|
||||
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl
|
||||
{
|
||||
// Resolve the local bundle URL
|
||||
NSString *path = [fileUrl substringFromIndex:7];
|
||||
|
||||
NSRange range = [path rangeOfString:@"." options:NSBackwardsSearch];
|
||||
|
||||
NSString *fileName = [path substringWithRange:NSMakeRange(0, range.location)];
|
||||
NSString *suffix = [path substringWithRange:NSMakeRange(range.location + 1, [path length] - [fileName length] - 1)];
|
||||
|
||||
return [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:fileName ofType:suffix]];
|
||||
}
|
||||
|
||||
- (void)parseItems:(xmlNodePtr)node
|
||||
{
|
||||
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
|
||||
|
||||
for (xmlNodePtr n = node->children; n != NULL; n = n->next) {
|
||||
NSString *nodeName = @((const char *)n->name);
|
||||
if ([nodeName isEqualToString:@"title"]) {
|
||||
item.title = [self contentForNode:n];
|
||||
} else if ([nodeName isEqualToString:@"enclosure"]) {
|
||||
NSString *url = [self contentForNodeAttribute:n attribute:"url"];
|
||||
|
||||
if ([url hasPrefix:@"file://"]) {
|
||||
item.url = [self parseLocalFileUrl:url];
|
||||
} else {
|
||||
item.url = [NSURL URLWithString:url];
|
||||
}
|
||||
} else if ([nodeName isEqualToString:@"link"]) {
|
||||
NSString *url = [self contentForNode:n];
|
||||
|
||||
if ([url hasPrefix:@"file://"]) {
|
||||
item.originatingUrl = [self parseLocalFileUrl:url];
|
||||
} else {
|
||||
item.originatingUrl = [NSURL URLWithString:url];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nil == item.url &&
|
||||
nil == item.originatingUrl) {
|
||||
// Not a valid item, as there is no URL. Skip.
|
||||
return;
|
||||
}
|
||||
|
||||
[_playlistItems addObject:item];
|
||||
}
|
||||
|
||||
- (void)parseResponseData
|
||||
{
|
||||
if (!_playlistItems) {
|
||||
_playlistItems = [[NSMutableArray alloc] init];
|
||||
}
|
||||
[_playlistItems removeAllObjects];
|
||||
|
||||
// RSS feed publication date format:
|
||||
// Sun, 22 Jul 2012 17:35:05 GMT
|
||||
[_dateFormatter setDateFormat:@"EEE, dd MMMM yyyy HH:mm:ss V"];
|
||||
[_dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"]];
|
||||
|
||||
[self performXPathQuery:kXPathQueryItems];
|
||||
}
|
||||
|
||||
- (void)parseXMLNode:(xmlNodePtr)node xPathQuery:(NSString *)xPathQuery
|
||||
{
|
||||
if ([xPathQuery isEqualToString:kXPathQueryItems]) {
|
||||
[self parseItems:node];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)playlistItems
|
||||
{
|
||||
return _playlistItems;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* A playlist item. Each item has a title and url.
|
||||
*/
|
||||
@interface FSPlaylistItem : NSObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* The title of the playlist item.
|
||||
*/
|
||||
@property (nonatomic,copy) NSString *title;
|
||||
/**
|
||||
* The URL of the playlist item.
|
||||
*/
|
||||
@property (nonatomic,copy) NSURL *url;
|
||||
/**
|
||||
* The originating URL of the playlist item.
|
||||
*/
|
||||
@property (nonatomic,copy) NSURL *originatingUrl;
|
||||
|
||||
/**
|
||||
* The number of bytes of audio data. Notice that this may differ
|
||||
* from the number of bytes the server returns for the content length!
|
||||
* For instance audio file meta data is excluded from the count.
|
||||
* Effectively you can use this property for seeking calculations.
|
||||
*
|
||||
* The property is only available for non-continuous streams which
|
||||
* have been in the "playing" state.
|
||||
*/
|
||||
@property (nonatomic,assign) UInt64 audioDataByteCount;
|
||||
|
||||
@end
|
||||
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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 "FSPlaylistItem.h"
|
||||
|
||||
@implementation FSPlaylistItem
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
FSPlaylistItem *otherObject = anObject;
|
||||
|
||||
if ([otherObject.title isEqual:self.title] &&
|
||||
[otherObject.url isEqual:self.url]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
typedef struct _xmlDoc xmlDoc;
|
||||
typedef xmlDoc *xmlDocPtr;
|
||||
|
||||
typedef struct _xmlNode xmlNode;
|
||||
typedef xmlNode *xmlNodePtr;
|
||||
|
||||
/**
|
||||
* XML HTTP request error status.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, FSXMLHttpRequestError) {
|
||||
/**
|
||||
* No error.
|
||||
*/
|
||||
FSXMLHttpRequestError_NoError = 0,
|
||||
/**
|
||||
* Connection failed.
|
||||
*/
|
||||
FSXMLHttpRequestError_Connection_Failed,
|
||||
/**
|
||||
* Invalid HTTP status.
|
||||
*/
|
||||
FSXMLHttpRequestError_Invalid_Http_Status,
|
||||
/**
|
||||
* XML parser failed.
|
||||
*/
|
||||
FSXMLHttpRequestError_XML_Parser_Failed
|
||||
};
|
||||
|
||||
/**
|
||||
* FSXMLHttpRequest is a class for retrieving data in the XML
|
||||
* format over a HTTP or HTTPS connection. It provides
|
||||
* the necessary foundation for parsing the retrieved XML data.
|
||||
* This class is not meant to be used directly but subclassed
|
||||
* to a specific requests.
|
||||
*
|
||||
* The usage pattern is the following:
|
||||
*
|
||||
* 1. Specify the URL with the url property.
|
||||
* 2. Define the onCompletion and onFailure handlers.
|
||||
* 3. Call the start method.
|
||||
*/
|
||||
@interface FSXMLHttpRequest : NSObject {
|
||||
NSURLSessionTask *_task;
|
||||
xmlDocPtr _xmlDocument;
|
||||
NSDateFormatter *_dateFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL of the request.
|
||||
*/
|
||||
@property (nonatomic,copy) NSURL *url;
|
||||
/**
|
||||
* Called upon completion of the request.
|
||||
*/
|
||||
@property (copy) void (^onCompletion)(void);
|
||||
/**
|
||||
* Called upon a failure.
|
||||
*/
|
||||
@property (copy) void (^onFailure)(void);
|
||||
/**
|
||||
* If the request fails, contains the latest error status.
|
||||
*/
|
||||
@property (readonly) FSXMLHttpRequestError lastError;
|
||||
|
||||
/**
|
||||
* Starts the request.
|
||||
*/
|
||||
- (void)start;
|
||||
/**
|
||||
* Cancels the request.
|
||||
*/
|
||||
- (void)cancel;
|
||||
|
||||
/**
|
||||
* Performs an XPath query on the parsed XML data.
|
||||
* Yields a parseXMLNode method call, which must be
|
||||
* defined in the subclasses.
|
||||
*
|
||||
* @param query The XPath query to be performed.
|
||||
*/
|
||||
- (NSArray *)performXPathQuery:(NSString *)query;
|
||||
/**
|
||||
* Retrieves content for the given XML node.
|
||||
*
|
||||
* @param node The node for content retreval.
|
||||
*/
|
||||
- (NSString *)contentForNode:(xmlNodePtr)node;
|
||||
/**
|
||||
* Retrieves content for the given XML node attribute.
|
||||
*
|
||||
* @param node The node for content retrieval.
|
||||
* @param attr The attribute from which the content is retrieved.
|
||||
*/
|
||||
- (NSString *)contentForNodeAttribute:(xmlNodePtr)node attribute:(const char *)attr;
|
||||
/**
|
||||
* Retrieves date from the given XML node.
|
||||
*
|
||||
* @param node The node for retrieving the date.
|
||||
*/
|
||||
- (NSDate *)dateFromNode:(xmlNodePtr)node;
|
||||
|
||||
@end
|
||||
@ -1,255 +0,0 @@
|
||||
/*
|
||||
* 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 "FSXMLHttpRequest.h"
|
||||
|
||||
#import <libxml/parser.h>
|
||||
#import <libxml/xpath.h>
|
||||
|
||||
#define DATE_COMPONENTS (NSYearCalendarUnit| NSMonthCalendarUnit | NSDayCalendarUnit | NSWeekCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit | NSWeekdayCalendarUnit | NSWeekdayOrdinalCalendarUnit)
|
||||
#define CURRENT_CALENDAR [NSCalendar currentCalendar]
|
||||
|
||||
@interface FSXMLHttpRequest (PrivateMethods)
|
||||
- (const char *)detectEncoding;
|
||||
- (void)parseResponseData;
|
||||
- (void)parseXMLNode:(xmlNodePtr)node xPathQuery:(NSString *)xPathQuery;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FSXMLHttpRequest
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_dateFormatter = [[NSDateFormatter alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
|
||||
if (_task) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastError = FSXMLHttpRequestError_NoError;
|
||||
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:self.url
|
||||
cachePolicy:NSURLRequestUseProtocolCachePolicy
|
||||
timeoutInterval:10.0];
|
||||
|
||||
NSURLSession *session = [NSURLSession sharedSession];
|
||||
|
||||
__weak FSXMLHttpRequest *weakSelf = self;
|
||||
|
||||
@synchronized (self) {
|
||||
_task = [session dataTaskWithRequest:request
|
||||
completionHandler:
|
||||
^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
FSXMLHttpRequest *strongSelf = weakSelf;
|
||||
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
|
||||
if(error) {
|
||||
strongSelf->_lastError = FSXMLHttpRequestError_Connection_Failed;
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSXMLHttpRequest: Request failed for URL: %@, error %@", strongSelf.url, [error localizedDescription]);
|
||||
#endif
|
||||
dispatch_async(dispatch_get_main_queue(), ^(){
|
||||
strongSelf.onFailure();
|
||||
});
|
||||
} else {
|
||||
if (httpResponse.statusCode != 200) {
|
||||
strongSelf->_lastError = FSXMLHttpRequestError_Invalid_Http_Status;
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSXMLHttpRequest: Unable to receive content for URL: %@", strongSelf.url);
|
||||
#endif
|
||||
dispatch_async(dispatch_get_main_queue(), ^(){
|
||||
strongSelf.onFailure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const char *encoding = [self detectEncoding:data];
|
||||
|
||||
strongSelf->_xmlDocument = xmlReadMemory([data bytes],
|
||||
(int)[data length],
|
||||
"",
|
||||
encoding,
|
||||
0);
|
||||
|
||||
if (!strongSelf->_xmlDocument) {
|
||||
strongSelf->_lastError = FSXMLHttpRequestError_XML_Parser_Failed;
|
||||
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
NSLog(@"FSXMLHttpRequest: Unable to parse the content for URL: %@", strongSelf.url);
|
||||
#endif
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^(){
|
||||
strongSelf.onFailure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
[strongSelf parseResponseData];
|
||||
|
||||
xmlFreeDoc(strongSelf->_xmlDocument);
|
||||
strongSelf->_xmlDocument = nil;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^(){
|
||||
strongSelf.onCompletion();
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
[_task resume];
|
||||
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
if (!_task) {
|
||||
return;
|
||||
}
|
||||
@synchronized (self) {
|
||||
[_task cancel];
|
||||
_task = nil;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* XML handling
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (NSArray *)performXPathQuery:(NSString *)query
|
||||
{
|
||||
NSMutableArray *resultNodes = [NSMutableArray array];
|
||||
xmlXPathContextPtr xpathCtx = NULL;
|
||||
xmlXPathObjectPtr xpathObj = NULL;
|
||||
|
||||
xpathCtx = xmlXPathNewContext(_xmlDocument);
|
||||
if (xpathCtx == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
xpathObj = xmlXPathEvalExpression((xmlChar *)[query cStringUsingEncoding:NSUTF8StringEncoding], xpathCtx);
|
||||
if (xpathObj == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
xmlNodeSetPtr nodes = xpathObj->nodesetval;
|
||||
if (!nodes) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < nodes->nodeNr; i++) {
|
||||
[self parseXMLNode:nodes->nodeTab[i] xPathQuery:query];
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (xpathObj) {
|
||||
xmlXPathFreeObject(xpathObj);
|
||||
}
|
||||
if (xpathCtx) {
|
||||
xmlXPathFreeContext(xpathCtx);
|
||||
}
|
||||
return resultNodes;
|
||||
}
|
||||
|
||||
- (NSString *)contentForNode:(xmlNodePtr)node
|
||||
{
|
||||
NSString *stringWithContent;
|
||||
if (!node) {
|
||||
stringWithContent = [[NSString alloc] init];
|
||||
} else {
|
||||
xmlChar *content = xmlNodeGetContent(node);
|
||||
if (!content) {
|
||||
return stringWithContent;
|
||||
}
|
||||
stringWithContent = @((const char *)content);
|
||||
xmlFree(content);
|
||||
}
|
||||
return stringWithContent;
|
||||
}
|
||||
|
||||
- (NSString *)contentForNodeAttribute:(xmlNodePtr)node attribute:(const char *)attr
|
||||
{
|
||||
NSString *stringWithContent;
|
||||
if (!node) {
|
||||
stringWithContent = [[NSString alloc] init];
|
||||
} else {
|
||||
xmlChar *content = xmlGetProp(node, (const xmlChar *)attr);
|
||||
if (!content) {
|
||||
return stringWithContent;
|
||||
}
|
||||
stringWithContent = @((const char *)content);
|
||||
xmlFree(content);
|
||||
}
|
||||
return stringWithContent;
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Helpers
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
- (const char *)detectEncoding:(NSData *)receivedData
|
||||
{
|
||||
const char *encoding = 0;
|
||||
const char *header = strndup([receivedData bytes], 60);
|
||||
|
||||
if (strstr(header, "utf-8") || strstr(header, "UTF-8")) {
|
||||
encoding = "UTF-8";
|
||||
} else if (strstr(header, "iso-8859-1") || strstr(header, "ISO-8859-1")) {
|
||||
encoding = "ISO-8859-1";
|
||||
}
|
||||
|
||||
free((void *)header);
|
||||
return encoding;
|
||||
}
|
||||
|
||||
- (NSDate *)dateFromNode:(xmlNodePtr)node
|
||||
{
|
||||
NSString *dateString = [self contentForNode:node];
|
||||
|
||||
/*
|
||||
* For some NSDateFormatter date parsing oddities: http://www.openradar.me/9944011
|
||||
*
|
||||
* Engineering has determined that this issue behaves as intended based on the following information:
|
||||
*
|
||||
* This is an intentional change in iOS 5. The issue is this: With the short formats as specified by z (=zzz) or v (=vvv),
|
||||
* there can be a lot of ambiguity. For example, "ET" for Eastern Time" could apply to different time zones in many different regions.
|
||||
* To improve formatting and parsing reliability, the short forms are only used in a locale if the "cu" (commonly used) flag is set
|
||||
* for the locale. Otherwise, only the long forms are used (for both formatting and parsing). This is a change in
|
||||
* open-source CLDR 2.0 / ICU 4.8, which is the basis for the ICU in iOS 5, which in turn is the basis of NSDateFormatter behavior.
|
||||
*
|
||||
* For the "en" locale (= "en_US"), the cu flag is set for metazones such as Alaska, America_Central, America_Eastern, America_Mountain,
|
||||
* America_Pacific, Atlantic, Hawaii_Aleutian, and GMT. It is not set for Europe_Central.
|
||||
*
|
||||
* However, for the "en_GB" locale, the cu flag is set for Europe_Central.
|
||||
*
|
||||
* So, a formatter set for short timezone style "z" or "zzz" and locale "en" or "en_US" will not parse "CEST" or "CET", but if the
|
||||
* locale is instead set to "en_GB" it will parse those. The "GMT" style will be parsed by all.
|
||||
*
|
||||
* If the formatter is set for the long timezone style "zzzz", and the locale is any of "en", "en_US", or "en_GB", then any of the
|
||||
* following will be parsed, because they are unambiguous:
|
||||
*
|
||||
* "Pacific Daylight Time" "Central European Summer Time" "Central European Time"
|
||||
*
|
||||
*/
|
||||
|
||||
return [_dateFormatter dateFromString:dateString];
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,566 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "audio_queue.h"
|
||||
#include "stream_configuration.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
//#define AQ_DEBUG 1
|
||||
//#define AQ_DEBUG_LOCKS 1
|
||||
|
||||
#if !defined (AQ_DEBUG)
|
||||
#define AQ_TRACE(...) do {} while (0)
|
||||
#define AQ_ASSERT(...) do {} while (0)
|
||||
#else
|
||||
#include <cassert>
|
||||
|
||||
#define AQ_TRACE(...) printf(__VA_ARGS__)
|
||||
#define AQ_ASSERT(...) assert(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
#if !defined (AQ_DEBUG_LOCKS)
|
||||
#define AQ_LOCK_TRACE(...) do {} while (0)
|
||||
#else
|
||||
#define AQ_LOCK_TRACE(...) printf(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
/* public */
|
||||
|
||||
Audio_Queue::Audio_Queue()
|
||||
: m_delegate(0),
|
||||
m_state(IDLE),
|
||||
m_outAQ(0),
|
||||
m_fillBufferIndex(0),
|
||||
m_bytesFilled(0),
|
||||
m_packetsFilled(0),
|
||||
m_buffersUsed(0),
|
||||
m_audioQueueStarted(false),
|
||||
m_levelMeteringEnabled(false),
|
||||
m_lastError(noErr),
|
||||
m_initialOutputVolume(1.0)
|
||||
{
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
m_audioQueueBuffer = new AudioQueueBufferRef[config->bufferCount];
|
||||
m_packetDescs = new AudioStreamPacketDescription[config->maxPacketDescs];
|
||||
m_bufferInUse = new bool[config->bufferCount];
|
||||
|
||||
for (size_t i=0; i < config->bufferCount; i++) {
|
||||
m_bufferInUse[i] = false;
|
||||
}
|
||||
|
||||
if (pthread_mutex_init(&m_mutex, NULL) != 0) {
|
||||
AQ_TRACE("m_mutex init failed!\n");
|
||||
}
|
||||
|
||||
if (pthread_mutex_init(&m_bufferInUseMutex, NULL) != 0) {
|
||||
AQ_TRACE("m_bufferInUseMutex init failed!\n");
|
||||
}
|
||||
|
||||
if (pthread_cond_init(&m_bufferFreeCondition, NULL) != 0) {
|
||||
AQ_TRACE("m_bufferFreeCondition init failed!\n");
|
||||
}
|
||||
}
|
||||
|
||||
Audio_Queue::~Audio_Queue()
|
||||
{
|
||||
stop(true);
|
||||
|
||||
cleanup();
|
||||
|
||||
delete [] m_audioQueueBuffer;
|
||||
delete [] m_packetDescs;
|
||||
delete [] m_bufferInUse;
|
||||
|
||||
pthread_mutex_destroy(&m_mutex);
|
||||
pthread_mutex_destroy(&m_bufferInUseMutex);
|
||||
pthread_cond_destroy(&m_bufferFreeCondition);
|
||||
}
|
||||
|
||||
bool Audio_Queue::initialized()
|
||||
{
|
||||
return (m_outAQ != 0);
|
||||
}
|
||||
|
||||
void Audio_Queue::start()
|
||||
{
|
||||
// start the queue if it has not been started already
|
||||
if (m_audioQueueStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
OSStatus err = AudioQueueStart(m_outAQ, NULL);
|
||||
if (!err) {
|
||||
m_audioQueueStarted = true;
|
||||
m_levelMeteringEnabled = false;
|
||||
m_lastError = noErr;
|
||||
} else {
|
||||
AQ_TRACE("%s: AudioQueueStart failed!\n", __PRETTY_FUNCTION__);
|
||||
m_lastError = err;
|
||||
}
|
||||
}
|
||||
|
||||
void Audio_Queue::pause()
|
||||
{
|
||||
if (m_state == RUNNING) {
|
||||
if (AudioQueuePause(m_outAQ) != 0) {
|
||||
AQ_TRACE("%s: AudioQueuePause failed!\n", __PRETTY_FUNCTION__);
|
||||
}
|
||||
setState(PAUSED);
|
||||
} else if (m_state == PAUSED) {
|
||||
AudioQueueStart(m_outAQ, NULL);
|
||||
setState(RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
void Audio_Queue::stop()
|
||||
{
|
||||
stop(true);
|
||||
}
|
||||
|
||||
float Audio_Queue::volume()
|
||||
{
|
||||
if (!m_outAQ) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float vol;
|
||||
|
||||
OSStatus err = AudioQueueGetParameter(m_outAQ, kAudioQueueParam_Volume, &vol);
|
||||
|
||||
if (!err) {
|
||||
return vol;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
void Audio_Queue::setVolume(float volume)
|
||||
{
|
||||
if (!m_outAQ) {
|
||||
return;
|
||||
}
|
||||
AudioQueueSetParameter(m_outAQ, kAudioQueueParam_Volume, volume);
|
||||
}
|
||||
|
||||
void Audio_Queue::setPlayRate(float playRate)
|
||||
{
|
||||
Stream_Configuration *configuration = Stream_Configuration::configuration();
|
||||
|
||||
if (!configuration->enableTimeAndPitchConversion) {
|
||||
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
|
||||
printf("*** FreeStreamer notification: Trying to set play rate for audio queue but enableTimeAndPitchConversion is disabled from configuration. Play rate settign will not work.\n");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_outAQ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (playRate < 0.5) {
|
||||
playRate = 0.5;
|
||||
}
|
||||
if (playRate > 2.0) {
|
||||
playRate = 2.0;
|
||||
}
|
||||
|
||||
AudioQueueSetParameter(m_outAQ, kAudioQueueParam_PlayRate, playRate);
|
||||
}
|
||||
|
||||
void Audio_Queue::stop(bool stopImmediately)
|
||||
{
|
||||
if (!m_audioQueueStarted) {
|
||||
AQ_TRACE("%s: audio queue already stopped, return!\n", __PRETTY_FUNCTION__);
|
||||
return;
|
||||
}
|
||||
m_audioQueueStarted = false;
|
||||
m_levelMeteringEnabled = false;
|
||||
|
||||
pthread_mutex_lock(&m_bufferInUseMutex);
|
||||
pthread_cond_signal(&m_bufferFreeCondition);
|
||||
pthread_mutex_unlock(&m_bufferInUseMutex);
|
||||
|
||||
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
|
||||
|
||||
if (AudioQueueFlush(m_outAQ) != 0) {
|
||||
AQ_TRACE("%s: AudioQueueFlush failed!\n", __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
if (stopImmediately) {
|
||||
AudioQueueRemovePropertyListener(m_outAQ,
|
||||
kAudioQueueProperty_IsRunning,
|
||||
audioQueueIsRunningCallback,
|
||||
this);
|
||||
}
|
||||
|
||||
if (AudioQueueStop(m_outAQ, stopImmediately) != 0) {
|
||||
AQ_TRACE("%s: AudioQueueStop failed!\n", __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
if (stopImmediately) {
|
||||
setState(IDLE);
|
||||
}
|
||||
|
||||
AQ_TRACE("%s: leave\n", __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
AudioTimeStamp Audio_Queue::currentTime()
|
||||
{
|
||||
AudioTimeStamp queueTime;
|
||||
Boolean discontinuity;
|
||||
|
||||
memset(&queueTime, 0, sizeof queueTime);
|
||||
|
||||
OSStatus err = AudioQueueGetCurrentTime(m_outAQ, NULL, &queueTime, &discontinuity);
|
||||
if (err) {
|
||||
AQ_TRACE("AudioQueueGetCurrentTime failed\n");
|
||||
}
|
||||
|
||||
return queueTime;
|
||||
}
|
||||
|
||||
AudioQueueLevelMeterState Audio_Queue::levels()
|
||||
{
|
||||
if (!m_levelMeteringEnabled) {
|
||||
UInt32 enabledLevelMeter = true;
|
||||
AudioQueueSetProperty(m_outAQ,
|
||||
kAudioQueueProperty_EnableLevelMetering,
|
||||
&enabledLevelMeter,
|
||||
sizeof(UInt32));
|
||||
|
||||
m_levelMeteringEnabled = true;
|
||||
}
|
||||
|
||||
AudioQueueLevelMeterState levelMeter;
|
||||
UInt32 levelMeterSize = sizeof(AudioQueueLevelMeterState);
|
||||
AudioQueueGetProperty(m_outAQ, kAudioQueueProperty_CurrentLevelMeterDB, &levelMeter, &levelMeterSize);
|
||||
return levelMeter;
|
||||
}
|
||||
|
||||
void Audio_Queue::init()
|
||||
{
|
||||
OSStatus err = noErr;
|
||||
|
||||
cleanup();
|
||||
|
||||
// create the audio queue
|
||||
err = AudioQueueNewOutput(&m_streamDesc, audioQueueOutputCallback, this, CFRunLoopGetCurrent(), NULL, 0, &m_outAQ);
|
||||
if (err) {
|
||||
AQ_TRACE("%s: error in AudioQueueNewOutput\n", __PRETTY_FUNCTION__);
|
||||
|
||||
m_lastError = err;
|
||||
|
||||
if (m_delegate) {
|
||||
m_delegate->audioQueueInitializationFailed();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Stream_Configuration *configuration = Stream_Configuration::configuration();
|
||||
|
||||
// allocate audio queue buffers
|
||||
for (unsigned int i = 0; i < configuration->bufferCount; ++i) {
|
||||
err = AudioQueueAllocateBuffer(m_outAQ, configuration->bufferSize, &m_audioQueueBuffer[i]);
|
||||
if (err) {
|
||||
/* If allocating the buffers failed, everything else will fail, too.
|
||||
* Dispose the queue so that we can later on detect that this
|
||||
* queue in fact has not been initialized.
|
||||
*/
|
||||
|
||||
AQ_TRACE("%s: error in AudioQueueAllocateBuffer\n", __PRETTY_FUNCTION__);
|
||||
|
||||
(void)AudioQueueDispose(m_outAQ, true);
|
||||
m_outAQ = 0;
|
||||
|
||||
m_lastError = err;
|
||||
|
||||
if (m_delegate) {
|
||||
m_delegate->audioQueueInitializationFailed();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// listen for kAudioQueueProperty_IsRunning
|
||||
err = AudioQueueAddPropertyListener(m_outAQ, kAudioQueueProperty_IsRunning, audioQueueIsRunningCallback, this);
|
||||
if (err) {
|
||||
AQ_TRACE("%s: error in AudioQueueAddPropertyListener\n", __PRETTY_FUNCTION__);
|
||||
m_lastError = err;
|
||||
return;
|
||||
}
|
||||
|
||||
if (configuration->enableTimeAndPitchConversion) {
|
||||
UInt32 enableTimePitchConversion = 1;
|
||||
|
||||
err = AudioQueueSetProperty (m_outAQ, kAudioQueueProperty_EnableTimePitch, &enableTimePitchConversion, sizeof(enableTimePitchConversion));
|
||||
if (err != noErr) {
|
||||
AQ_TRACE("Failed to enable time and pitch conversion. Play rate setting will fail\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (m_initialOutputVolume != 1.0) {
|
||||
setVolume(m_initialOutputVolume);
|
||||
}
|
||||
}
|
||||
|
||||
void Audio_Queue::handleAudioPackets(UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescriptions)
|
||||
{
|
||||
if (!initialized()) {
|
||||
AQ_TRACE("%s: warning: attempt to handle audio packets with uninitialized audio queue. return.\n", __PRETTY_FUNCTION__);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// this is called by audio file stream when it finds packets of audio
|
||||
AQ_TRACE("got data. bytes: %u packets: %u\n", inNumberBytes, (unsigned int)inNumberPackets);
|
||||
|
||||
/* Place each packet into a buffer and then send each buffer into the audio
|
||||
queue */
|
||||
UInt32 i;
|
||||
|
||||
for (i = 0; i < inNumberPackets; i++) {
|
||||
AudioStreamPacketDescription *desc = &inPacketDescriptions[i];
|
||||
|
||||
const void *data = (const char*)inInputData + desc->mStartOffset;
|
||||
|
||||
if (!initialized()) {
|
||||
AQ_TRACE("%s: warning: attempt to handle audio packets with uninitialized audio queue. return.\n", __PRETTY_FUNCTION__);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
|
||||
|
||||
UInt32 packetSize = desc->mDataByteSize;
|
||||
|
||||
/* This shouldn't happen because most of the time we read the packet buffer
|
||||
size from the file stream, but if we restored to guessing it we could
|
||||
come up too small here */
|
||||
if (packetSize > config->bufferSize) {
|
||||
AQ_TRACE("%s: packetSize %u > AQ_BUFSIZ %li\n", __PRETTY_FUNCTION__, (unsigned int)packetSize, config->bufferSize);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the space remaining in the buffer is not enough for this packet, then
|
||||
// enqueue the buffer and wait for another to become available.
|
||||
if (config->bufferSize - m_bytesFilled < packetSize) {
|
||||
enqueueBuffer();
|
||||
|
||||
if (!m_audioQueueStarted) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
AQ_TRACE("%s: skipped enqueueBuffer AQ_BUFSIZ - m_bytesFilled %lu, packetSize %u\n", __PRETTY_FUNCTION__, (config->bufferSize - m_bytesFilled), (unsigned int)packetSize);
|
||||
}
|
||||
|
||||
// copy data to the audio queue buffer
|
||||
AudioQueueBufferRef buf = m_audioQueueBuffer[m_fillBufferIndex];
|
||||
memcpy((char*)buf->mAudioData, data, packetSize);
|
||||
|
||||
// fill out packet description to pass to enqueue() later on
|
||||
m_packetDescs[m_packetsFilled] = *desc;
|
||||
// Make sure the offset is relative to the start of the audio buffer
|
||||
m_packetDescs[m_packetsFilled].mStartOffset = m_bytesFilled;
|
||||
// keep track of bytes filled and packets filled
|
||||
m_bytesFilled += packetSize;
|
||||
m_packetsFilled++;
|
||||
|
||||
/* If filled our buffer with packets, then commit it to the system */
|
||||
if (m_packetsFilled >= config->maxPacketDescs) {
|
||||
enqueueBuffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* private */
|
||||
|
||||
void Audio_Queue::cleanup()
|
||||
{
|
||||
if (!initialized()) {
|
||||
AQ_TRACE("%s: warning: attempt to cleanup an uninitialized audio queue. return.\n", __PRETTY_FUNCTION__);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
if (m_state != IDLE) {
|
||||
AQ_TRACE("%s: attemping to cleanup the audio queue when it is still playing, force stopping\n",
|
||||
__PRETTY_FUNCTION__);
|
||||
|
||||
AudioQueueRemovePropertyListener(m_outAQ,
|
||||
kAudioQueueProperty_IsRunning,
|
||||
audioQueueIsRunningCallback,
|
||||
this);
|
||||
|
||||
AudioQueueStop(m_outAQ, true);
|
||||
setState(IDLE);
|
||||
}
|
||||
|
||||
if (AudioQueueDispose(m_outAQ, true) != 0) {
|
||||
AQ_TRACE("%s: AudioQueueDispose failed!\n", __PRETTY_FUNCTION__);
|
||||
}
|
||||
m_outAQ = 0;
|
||||
m_fillBufferIndex = m_bytesFilled = m_packetsFilled = m_buffersUsed = 0;
|
||||
|
||||
for (size_t i=0; i < config->bufferCount; i++) {
|
||||
m_bufferInUse[i] = false;
|
||||
}
|
||||
|
||||
m_lastError = noErr;
|
||||
}
|
||||
|
||||
void Audio_Queue::setState(State state)
|
||||
{
|
||||
if (m_state == state) {
|
||||
/* We are already in this state! */
|
||||
return;
|
||||
}
|
||||
|
||||
m_state = state;
|
||||
|
||||
if (m_delegate) {
|
||||
m_delegate->audioQueueStateChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
void Audio_Queue::enqueueBuffer()
|
||||
{
|
||||
AQ_ASSERT(!m_bufferInUse[m_fillBufferIndex]);
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
|
||||
|
||||
pthread_mutex_lock(&m_bufferInUseMutex);
|
||||
|
||||
m_bufferInUse[m_fillBufferIndex] = true;
|
||||
m_buffersUsed++;
|
||||
|
||||
// enqueue buffer
|
||||
AudioQueueBufferRef fillBuf = m_audioQueueBuffer[m_fillBufferIndex];
|
||||
fillBuf->mAudioDataByteSize = m_bytesFilled;
|
||||
|
||||
pthread_mutex_unlock(&m_bufferInUseMutex);
|
||||
|
||||
AQ_ASSERT(m_packetsFilled > 0);
|
||||
OSStatus err = AudioQueueEnqueueBuffer(m_outAQ, fillBuf, m_packetsFilled, m_packetDescs);
|
||||
|
||||
if (!err) {
|
||||
m_lastError = noErr;
|
||||
start();
|
||||
} else {
|
||||
/* If we get an error here, it very likely means that the audio queue is no longer
|
||||
running */
|
||||
AQ_TRACE("%s: error in AudioQueueEnqueueBuffer\n", __PRETTY_FUNCTION__);
|
||||
m_lastError = err;
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&m_bufferInUseMutex);
|
||||
// go to next buffer
|
||||
if (++m_fillBufferIndex >= config->bufferCount) {
|
||||
m_fillBufferIndex = 0;
|
||||
}
|
||||
// reset bytes filled
|
||||
m_bytesFilled = 0;
|
||||
// reset packets filled
|
||||
m_packetsFilled = 0;
|
||||
|
||||
// wait until next buffer is not in use
|
||||
|
||||
while (m_bufferInUse[m_fillBufferIndex]) {
|
||||
AQ_TRACE("waiting for buffer %u\n", (unsigned int)m_fillBufferIndex);
|
||||
|
||||
pthread_cond_wait(&m_bufferFreeCondition, &m_bufferInUseMutex);
|
||||
}
|
||||
pthread_mutex_unlock(&m_bufferInUseMutex);
|
||||
}
|
||||
|
||||
// this is called by the audio queue when it has finished decoding our data.
|
||||
// The buffer is now free to be reused.
|
||||
void Audio_Queue::audioQueueOutputCallback(void *inClientData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
|
||||
{
|
||||
Audio_Queue *audioQueue = static_cast<Audio_Queue*>(inClientData);
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
int bufIndex = -1;
|
||||
|
||||
for (unsigned int i = 0; i < config->bufferCount; ++i) {
|
||||
if (inBuffer == audioQueue->m_audioQueueBuffer[i]) {
|
||||
AQ_TRACE("findQueueBuffer %i\n", i);
|
||||
bufIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bufIndex == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&audioQueue->m_bufferInUseMutex);
|
||||
|
||||
AQ_ASSERT(audioQueue->m_bufferInUse[bufIndex]);
|
||||
|
||||
audioQueue->m_bufferInUse[bufIndex] = false;
|
||||
audioQueue->m_buffersUsed--;
|
||||
|
||||
AQ_TRACE("signaling buffer free for inuse %i....\n", bufIndex);
|
||||
pthread_cond_signal(&audioQueue->m_bufferFreeCondition);
|
||||
AQ_TRACE("signal sent!\n");
|
||||
|
||||
if (audioQueue->m_buffersUsed == 0 && audioQueue->m_delegate) {
|
||||
AQ_LOCK_TRACE("audioQueueOutputCallback: unlock 2\n");
|
||||
pthread_mutex_unlock(&audioQueue->m_bufferInUseMutex);
|
||||
|
||||
if (audioQueue->m_delegate) {
|
||||
audioQueue->m_delegate->audioQueueBuffersEmpty();
|
||||
}
|
||||
} else {
|
||||
pthread_mutex_unlock(&audioQueue->m_bufferInUseMutex);
|
||||
|
||||
if (audioQueue->m_delegate) {
|
||||
audioQueue->m_delegate->audioQueueFinishedPlayingPacket();
|
||||
}
|
||||
}
|
||||
|
||||
AQ_LOCK_TRACE("audioQueueOutputCallback: unlock\n");
|
||||
}
|
||||
|
||||
void Audio_Queue::audioQueueIsRunningCallback(void *inClientData, AudioQueueRef inAQ, AudioQueuePropertyID inID)
|
||||
{
|
||||
Audio_Queue *audioQueue = static_cast<Audio_Queue*>(inClientData);
|
||||
|
||||
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
|
||||
|
||||
UInt32 running;
|
||||
UInt32 output = sizeof(running);
|
||||
OSStatus err = AudioQueueGetProperty(inAQ, kAudioQueueProperty_IsRunning, &running, &output);
|
||||
if (err) {
|
||||
AQ_TRACE("%s: error in kAudioQueueProperty_IsRunning\n", __PRETTY_FUNCTION__);
|
||||
return;
|
||||
}
|
||||
if (running) {
|
||||
AQ_TRACE("audio queue running!\n");
|
||||
audioQueue->setState(RUNNING);
|
||||
} else {
|
||||
audioQueue->setState(IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace astreamer
|
||||
@ -1,102 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_AUDIO_QUEUE_H
|
||||
#define ASTREAMER_AUDIO_QUEUE_H
|
||||
|
||||
#include <AudioToolbox/AudioToolbox.h> /* AudioFileStreamID */
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
class Audio_Queue_Delegate;
|
||||
struct queued_packet;
|
||||
|
||||
class Audio_Queue {
|
||||
public:
|
||||
Audio_Queue_Delegate *m_delegate;
|
||||
|
||||
enum State {
|
||||
IDLE,
|
||||
RUNNING,
|
||||
PAUSED
|
||||
};
|
||||
|
||||
Audio_Queue();
|
||||
virtual ~Audio_Queue();
|
||||
|
||||
bool initialized();
|
||||
|
||||
void init();
|
||||
|
||||
// Notice: the queue blocks if it has no free buffers
|
||||
void handleAudioPackets(UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescriptions);
|
||||
|
||||
void start();
|
||||
void pause();
|
||||
void stop(bool stopImmediately);
|
||||
void stop();
|
||||
|
||||
float volume();
|
||||
|
||||
void setVolume(float volume);
|
||||
void setPlayRate(float playRate);
|
||||
|
||||
AudioTimeStamp currentTime();
|
||||
AudioQueueLevelMeterState levels();
|
||||
|
||||
private:
|
||||
Audio_Queue(const Audio_Queue&);
|
||||
Audio_Queue& operator=(const Audio_Queue&);
|
||||
|
||||
State m_state;
|
||||
|
||||
AudioQueueRef m_outAQ; // the audio queue
|
||||
|
||||
AudioQueueBufferRef *m_audioQueueBuffer; // audio queue buffers
|
||||
AudioStreamPacketDescription *m_packetDescs; // packet descriptions for enqueuing audio
|
||||
|
||||
UInt32 m_fillBufferIndex; // the index of the audioQueueBuffer that is being filled
|
||||
UInt32 m_bytesFilled; // how many bytes have been filled
|
||||
UInt32 m_packetsFilled; // how many packets have been filled
|
||||
UInt32 m_buffersUsed; // how many buffers are used
|
||||
|
||||
bool m_audioQueueStarted; // flag to indicate that the queue has been started
|
||||
bool *m_bufferInUse; // flags to indicate that a buffer is still in use
|
||||
bool m_levelMeteringEnabled;
|
||||
|
||||
pthread_mutex_t m_mutex;
|
||||
|
||||
pthread_mutex_t m_bufferInUseMutex;
|
||||
pthread_cond_t m_bufferFreeCondition;
|
||||
|
||||
public:
|
||||
OSStatus m_lastError;
|
||||
AudioStreamBasicDescription m_streamDesc;
|
||||
float m_initialOutputVolume;
|
||||
|
||||
private:
|
||||
void cleanup();
|
||||
void setCookiesForStream(AudioFileStreamID inAudioFileStream);
|
||||
void setState(State state);
|
||||
void enqueueBuffer();
|
||||
|
||||
static void audioQueueOutputCallback(void *inClientData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer);
|
||||
static void audioQueueIsRunningCallback(void *inClientData, AudioQueueRef inAQ, AudioQueuePropertyID inID);
|
||||
};
|
||||
|
||||
class Audio_Queue_Delegate {
|
||||
public:
|
||||
virtual void audioQueueStateChanged(Audio_Queue::State state) = 0;
|
||||
virtual void audioQueueBuffersEmpty() = 0;
|
||||
virtual void audioQueueInitializationFailed() = 0;
|
||||
virtual void audioQueueFinishedPlayingPacket() = 0;
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_AUDIO_QUEUE_H
|
||||
2229
Pods/FreeStreamer/FreeStreamer/FreeStreamer/audio_stream.cpp
generated
2229
Pods/FreeStreamer/FreeStreamer/FreeStreamer/audio_stream.cpp
generated
File diff suppressed because it is too large
Load Diff
@ -1,255 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_AUDIO_STREAM_H
|
||||
#define ASTREAMER_AUDIO_STREAM_H
|
||||
|
||||
#import "input_stream.h"
|
||||
#include "audio_queue.h"
|
||||
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <list>
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
typedef struct queued_packet {
|
||||
UInt64 identifier;
|
||||
AudioStreamPacketDescription desc;
|
||||
struct queued_packet *next;
|
||||
char data[];
|
||||
} queued_packet_t;
|
||||
|
||||
typedef struct {
|
||||
float offset;
|
||||
float timePlayed;
|
||||
} AS_Playback_Position;
|
||||
|
||||
enum Audio_Stream_Error {
|
||||
AS_ERR_OPEN = 1, // Cannot open the audio stream
|
||||
AS_ERR_STREAM_PARSE = 2, // Parse error
|
||||
AS_ERR_NETWORK = 3, // Network error
|
||||
AS_ERR_UNSUPPORTED_FORMAT = 4,
|
||||
AS_ERR_BOUNCING = 5,
|
||||
AS_ERR_TERMINATED = 6
|
||||
};
|
||||
|
||||
class Audio_Stream_Delegate;
|
||||
class File_Output;
|
||||
|
||||
#define kAudioStreamBitrateBufferSize 50
|
||||
|
||||
class Audio_Stream : public Input_Stream_Delegate, public Audio_Queue_Delegate {
|
||||
public:
|
||||
Audio_Stream_Delegate *m_delegate;
|
||||
|
||||
enum State {
|
||||
STOPPED,
|
||||
BUFFERING,
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
SEEKING,
|
||||
FAILED,
|
||||
END_OF_FILE,
|
||||
PLAYBACK_COMPLETED
|
||||
};
|
||||
|
||||
Audio_Stream();
|
||||
virtual ~Audio_Stream();
|
||||
|
||||
void open();
|
||||
void open(Input_Stream_Position *position);
|
||||
void close(bool closeParser);
|
||||
void pause();
|
||||
void rewind(unsigned seconds);
|
||||
|
||||
void startCachedDataPlayback();
|
||||
|
||||
AS_Playback_Position playbackPosition();
|
||||
UInt64 audioDataByteCount();
|
||||
float durationInSeconds();
|
||||
void seekToOffset(float offset);
|
||||
|
||||
Input_Stream_Position streamPositionForOffset(float offset);
|
||||
|
||||
float currentVolume();
|
||||
void setDecoderRunState(bool decoderShouldRun);
|
||||
void setVolume(float volume);
|
||||
void setPlayRate(float playRate);
|
||||
|
||||
void setUrl(CFURLRef url);
|
||||
void setStrictContentTypeChecking(bool strictChecking);
|
||||
void setDefaultContentType(CFStringRef defaultContentType);
|
||||
void setSeekOffset(float offset);
|
||||
void setDefaultContentLength(UInt64 defaultContentLength);
|
||||
void setContentLength(UInt64 contentLength);
|
||||
void setPreloading(bool preloading);
|
||||
bool isPreloading();
|
||||
|
||||
void setOutputFile(CFURLRef url);
|
||||
CFURLRef outputFile();
|
||||
|
||||
State state();
|
||||
|
||||
CFStringRef sourceFormatDescription();
|
||||
CFStringRef contentType();
|
||||
|
||||
CFStringRef createCacheIdentifierForURL(CFURLRef url);
|
||||
|
||||
size_t cachedDataSize();
|
||||
bool strictContentTypeChecking();
|
||||
float bitrate();
|
||||
|
||||
UInt64 defaultContentLength();
|
||||
UInt64 contentLength();
|
||||
int playbackDataCount();
|
||||
|
||||
AudioQueueLevelMeterState levels();
|
||||
|
||||
/* Audio_Queue_Delegate */
|
||||
void audioQueueStateChanged(Audio_Queue::State state);
|
||||
void audioQueueBuffersEmpty();
|
||||
void audioQueueInitializationFailed();
|
||||
void audioQueueFinishedPlayingPacket();
|
||||
|
||||
/* Input_Stream_Delegate */
|
||||
void streamIsReadyRead();
|
||||
void streamHasBytesAvailable(UInt8 *data, UInt32 numBytes);
|
||||
void streamEndEncountered();
|
||||
void streamErrorOccurred(CFStringRef errorDesc);
|
||||
void streamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
|
||||
void streamMetaDataByteSizeAvailable(UInt32 sizeInBytes);
|
||||
|
||||
private:
|
||||
|
||||
Audio_Stream(const Audio_Stream&);
|
||||
Audio_Stream& operator=(const Audio_Stream&);
|
||||
|
||||
bool m_inputStreamRunning;
|
||||
bool m_audioStreamParserRunning;
|
||||
bool m_initialBufferingCompleted;
|
||||
bool m_discontinuity;
|
||||
bool m_preloading;
|
||||
bool m_audioQueueConsumedPackets;
|
||||
|
||||
UInt64 m_defaultContentLength;
|
||||
UInt64 m_contentLength;
|
||||
UInt64 m_originalContentLength;
|
||||
UInt64 m_bytesReceived;
|
||||
|
||||
State m_state;
|
||||
Input_Stream *m_inputStream;
|
||||
Audio_Queue *m_audioQueue;
|
||||
|
||||
CFRunLoopTimerRef m_watchdogTimer;
|
||||
CFRunLoopTimerRef m_seekTimer;
|
||||
CFRunLoopTimerRef m_inputStreamTimer;
|
||||
CFRunLoopTimerRef m_stateSetTimer;
|
||||
CFRunLoopTimerRef m_decodeTimer;
|
||||
|
||||
AudioFileStreamID m_audioFileStream; // the audio file stream parser
|
||||
AudioConverterRef m_audioConverter;
|
||||
AudioStreamBasicDescription m_srcFormat;
|
||||
AudioStreamBasicDescription m_dstFormat;
|
||||
OSStatus m_initializationError;
|
||||
|
||||
UInt32 m_outputBufferSize;
|
||||
UInt8 *m_outputBuffer;
|
||||
|
||||
UInt64 m_packetIdentifier;
|
||||
UInt64 m_playingPacketIdentifier;
|
||||
UInt64 m_dataOffset;
|
||||
float m_seekOffset;
|
||||
size_t m_bounceCount;
|
||||
CFAbsoluteTime m_firstBufferingTime;
|
||||
|
||||
bool m_strictContentTypeChecking;
|
||||
CFStringRef m_defaultContentType;
|
||||
CFStringRef m_contentType;
|
||||
|
||||
File_Output *m_fileOutput;
|
||||
|
||||
CFURLRef m_outputFile;
|
||||
|
||||
queued_packet_t *m_queuedHead;
|
||||
queued_packet_t *m_queuedTail;
|
||||
queued_packet_t *m_playPacket;
|
||||
|
||||
std::list <queued_packet_t*> m_processedPackets;
|
||||
|
||||
unsigned m_numPacketsToRewind;
|
||||
|
||||
size_t m_cachedDataSize;
|
||||
|
||||
UInt64 m_audioDataByteCount;
|
||||
UInt64 m_audioDataPacketCount;
|
||||
UInt32 m_bitRate;
|
||||
UInt32 m_metaDataSizeInBytes;
|
||||
|
||||
double m_packetDuration;
|
||||
double m_bitrateBuffer[kAudioStreamBitrateBufferSize];
|
||||
size_t m_bitrateBufferIndex;
|
||||
|
||||
float m_outputVolume;
|
||||
|
||||
bool m_converterRunOutOfData;
|
||||
bool m_decoderShouldRun;
|
||||
bool m_decoderFailed;
|
||||
bool m_decoderThreadCreated;
|
||||
|
||||
pthread_mutex_t m_packetQueueMutex;
|
||||
pthread_mutex_t m_streamStateMutex;
|
||||
|
||||
pthread_t m_decodeThread;
|
||||
|
||||
CFRunLoopRef m_decodeRunLoop;
|
||||
CFRunLoopRef m_mainRunLoop;
|
||||
|
||||
CFStringRef createHashForString(CFStringRef str);
|
||||
|
||||
Audio_Queue *audioQueue();
|
||||
void closeAudioQueue();
|
||||
|
||||
void closeAndSignalError(int error, CFStringRef errorDescription);
|
||||
void setState(State state);
|
||||
void setCookiesForStream(AudioFileStreamID inAudioFileStream);
|
||||
|
||||
void createWatchdogTimer();
|
||||
void invalidateWatchdogTimer();
|
||||
|
||||
int cachedDataCount();
|
||||
void determineBufferingLimits();
|
||||
void cleanupCachedData();
|
||||
|
||||
static void watchdogTimerCallback(CFRunLoopTimerRef timer, void *info);
|
||||
static void seekTimerCallback(CFRunLoopTimerRef timer, void *info);
|
||||
static void inputStreamTimerCallback(CFRunLoopTimerRef timer, void *info);
|
||||
static void stateSetTimerCallback(CFRunLoopTimerRef timer, void *info);
|
||||
|
||||
bool decoderShouldRun();
|
||||
static void decodeSinglePacket(CFRunLoopTimerRef timer, void *info);
|
||||
static void *decodeLoop(void *arg);
|
||||
|
||||
static OSStatus encoderDataCallback(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData);
|
||||
static void propertyValueCallback(void *inClientData, AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 *ioFlags);
|
||||
static void streamDataCallback(void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescriptions);
|
||||
|
||||
AudioFileTypeID audioStreamTypeFromContentType(CFStringRef contentType);
|
||||
};
|
||||
|
||||
class Audio_Stream_Delegate {
|
||||
public:
|
||||
virtual void audioStreamStateChanged(Audio_Stream::State state) = 0;
|
||||
virtual void audioStreamErrorOccurred(int errorCode, CFStringRef errorDescription) = 0;
|
||||
virtual void audioStreamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData) = 0;
|
||||
virtual void samplesAvailable(AudioBufferList *samples, UInt32 frames, AudioStreamPacketDescription description) = 0;
|
||||
virtual void bitrateAvailable() = 0;
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_AUDIO_STREAM_H
|
||||
@ -1,440 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "caching_stream.h"
|
||||
#include "file_output.h"
|
||||
#include "stream_configuration.h"
|
||||
#include "file_stream.h"
|
||||
|
||||
//#define CS_DEBUG 1
|
||||
|
||||
#if !defined (CS_DEBUG)
|
||||
#define CS_TRACE(...) do {} while (0)
|
||||
#define CS_TRACE_CFSTRING(X) do {} while (0)
|
||||
#define CS_TRACE_CFURL(X) do {} while (0)
|
||||
#else
|
||||
#define CS_TRACE(...) printf(__VA_ARGS__)
|
||||
#define CS_TRACE_CFSTRING(X) CS_TRACE("%s\n", CFStringGetCStringPtr(X, kCFStringEncodingMacRoman))
|
||||
#define CS_TRACE_CFURL(X) CS_TRACE_CFSTRING(CFURLGetString(X))
|
||||
#endif
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
Caching_Stream::Caching_Stream(Input_Stream *target) :
|
||||
m_target(target),
|
||||
m_fileOutput(0),
|
||||
m_fileStream(new File_Stream()),
|
||||
m_cacheable(false),
|
||||
m_writable(false),
|
||||
m_useCache(false),
|
||||
m_cacheMetaDataWritten(false),
|
||||
m_cacheIdentifier(0),
|
||||
m_fileUrl(0),
|
||||
m_metaDataUrl(0)
|
||||
{
|
||||
m_target->m_delegate = this;
|
||||
m_fileStream->m_delegate = this;
|
||||
}
|
||||
|
||||
Caching_Stream::~Caching_Stream()
|
||||
{
|
||||
if (m_target) {
|
||||
delete m_target;
|
||||
m_target = 0;
|
||||
}
|
||||
if (m_fileOutput) {
|
||||
delete m_fileOutput;
|
||||
m_fileOutput = 0;
|
||||
}
|
||||
if (m_fileStream) {
|
||||
delete m_fileStream;
|
||||
m_fileStream = 0;
|
||||
}
|
||||
if (m_cacheIdentifier) {
|
||||
CFRelease(m_cacheIdentifier);
|
||||
m_cacheIdentifier = 0;
|
||||
}
|
||||
if (m_fileUrl) {
|
||||
CFRelease(m_fileUrl);
|
||||
m_fileUrl = 0;
|
||||
}
|
||||
if (m_metaDataUrl) {
|
||||
CFRelease(m_metaDataUrl);
|
||||
m_fileUrl = 0;
|
||||
}
|
||||
}
|
||||
|
||||
CFURLRef Caching_Stream::createFileURLWithPath(CFStringRef path)
|
||||
{
|
||||
CFURLRef fileUrl = NULL;
|
||||
|
||||
if (!path) {
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, path, NULL, NULL, kCFStringEncodingUTF8);
|
||||
|
||||
CFURLRef regularUrl = CFURLCreateWithString(kCFAllocatorDefault, (escapedPath ? escapedPath : path), NULL);
|
||||
|
||||
if (regularUrl) {
|
||||
fileUrl = CFURLCreateFilePathURL(kCFAllocatorDefault, regularUrl, NULL);
|
||||
|
||||
CFRelease(regularUrl);
|
||||
}
|
||||
|
||||
if (escapedPath) {
|
||||
CFRelease(escapedPath);
|
||||
}
|
||||
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
void Caching_Stream::readMetaData()
|
||||
{
|
||||
if (!m_metaDataUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
CFReadStreamRef readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, m_metaDataUrl);
|
||||
|
||||
if (readStream) {
|
||||
if (CFReadStreamOpen(readStream)) {
|
||||
|
||||
UInt8 buf[1024];
|
||||
|
||||
CFIndex bytesRead = CFReadStreamRead(readStream, buf, 1024);
|
||||
|
||||
if (bytesRead > 0) {
|
||||
CFStringRef contentType = CFStringCreateWithBytes(kCFAllocatorDefault, buf, bytesRead, kCFStringEncodingUTF8, false);
|
||||
|
||||
if (contentType) {
|
||||
if (m_fileStream) {
|
||||
CS_TRACE("Setting the content type of the file stream based on the meta data\n");
|
||||
CS_TRACE_CFSTRING(contentType);
|
||||
|
||||
m_fileStream->setContentType(contentType);
|
||||
}
|
||||
|
||||
CFRelease(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
CFReadStreamClose(readStream);
|
||||
}
|
||||
|
||||
CFRelease(readStream);
|
||||
}
|
||||
}
|
||||
|
||||
Input_Stream_Position Caching_Stream::position()
|
||||
{
|
||||
if (m_useCache) {
|
||||
return m_fileStream->position();
|
||||
} else {
|
||||
return m_target->position();
|
||||
}
|
||||
}
|
||||
|
||||
CFStringRef Caching_Stream::contentType()
|
||||
{
|
||||
if (m_useCache) {
|
||||
return m_fileStream->contentType();
|
||||
} else {
|
||||
return m_target->contentType();
|
||||
}
|
||||
}
|
||||
|
||||
size_t Caching_Stream::contentLength()
|
||||
{
|
||||
if (m_useCache) {
|
||||
return m_fileStream->contentLength();
|
||||
} else {
|
||||
return m_target->contentLength();
|
||||
}
|
||||
}
|
||||
|
||||
bool Caching_Stream::open()
|
||||
{
|
||||
bool status;
|
||||
|
||||
if (CFURLResourceIsReachable(m_metaDataUrl, NULL) &&
|
||||
CFURLResourceIsReachable(m_fileUrl, NULL)) {
|
||||
m_cacheable = false;
|
||||
m_writable = false;
|
||||
m_useCache = true;
|
||||
m_cacheMetaDataWritten = false;
|
||||
|
||||
readMetaData();
|
||||
|
||||
CS_TRACE("Playing file from cache\n");
|
||||
CS_TRACE_CFURL(m_fileUrl);
|
||||
|
||||
status = m_fileStream->open();
|
||||
} else {
|
||||
m_cacheable = true;
|
||||
m_writable = false;
|
||||
m_useCache = false;
|
||||
m_cacheMetaDataWritten = false;
|
||||
|
||||
CS_TRACE("File not cached\n");
|
||||
|
||||
status = m_target->open();
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
bool Caching_Stream::open(const Input_Stream_Position& position)
|
||||
{
|
||||
bool status;
|
||||
|
||||
if (CFURLResourceIsReachable(m_metaDataUrl, NULL) &&
|
||||
CFURLResourceIsReachable(m_fileUrl, NULL)) {
|
||||
m_cacheable = false;
|
||||
m_writable = false;
|
||||
m_useCache = true;
|
||||
m_cacheMetaDataWritten = false;
|
||||
|
||||
readMetaData();
|
||||
|
||||
CS_TRACE("Playing file from cache\n");
|
||||
CS_TRACE_CFURL(m_fileUrl);
|
||||
|
||||
status = m_fileStream->open(position);
|
||||
} else {
|
||||
m_cacheable = false;
|
||||
m_writable = false;
|
||||
m_useCache = false;
|
||||
m_cacheMetaDataWritten = false;
|
||||
|
||||
CS_TRACE("File not cached\n");
|
||||
|
||||
status = m_target->open(position);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void Caching_Stream::close()
|
||||
{
|
||||
m_fileStream->close();
|
||||
m_target->close();
|
||||
}
|
||||
|
||||
void Caching_Stream::setScheduledInRunLoop(bool scheduledInRunLoop)
|
||||
{
|
||||
if (m_useCache) {
|
||||
m_fileStream->setScheduledInRunLoop(scheduledInRunLoop);
|
||||
} else {
|
||||
m_target->setScheduledInRunLoop(scheduledInRunLoop);
|
||||
}
|
||||
}
|
||||
|
||||
void Caching_Stream::setUrl(CFURLRef url)
|
||||
{
|
||||
m_target->setUrl(url);
|
||||
}
|
||||
|
||||
void Caching_Stream::setCacheIdentifier(CFStringRef cacheIdentifier)
|
||||
{
|
||||
m_cacheIdentifier = CFStringCreateCopy(kCFAllocatorDefault, cacheIdentifier);
|
||||
|
||||
if (m_fileOutput) {
|
||||
delete m_fileOutput;
|
||||
m_fileOutput = 0;
|
||||
}
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
CFStringRef filePath = CFStringCreateWithFormat(NULL, NULL, CFSTR("file://%@/%@"), config->cacheDirectory, m_cacheIdentifier);
|
||||
CFStringRef metaDataPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("file://%@/%@.metadata"), config->cacheDirectory, m_cacheIdentifier);
|
||||
|
||||
if (m_fileUrl) {
|
||||
CFRelease(m_fileUrl);
|
||||
m_fileUrl = 0;
|
||||
}
|
||||
if (m_metaDataUrl) {
|
||||
CFRelease(m_metaDataUrl);
|
||||
m_metaDataUrl = 0;
|
||||
}
|
||||
|
||||
m_fileUrl = createFileURLWithPath(filePath);
|
||||
m_metaDataUrl = createFileURLWithPath(metaDataPath);
|
||||
|
||||
m_fileStream->setUrl(m_fileUrl);
|
||||
|
||||
CFRelease(filePath);
|
||||
CFRelease(metaDataPath);
|
||||
}
|
||||
|
||||
bool Caching_Stream::canHandleUrl(CFURLRef url)
|
||||
{
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CFStringRef scheme = CFURLCopyScheme(url);
|
||||
|
||||
if (scheme) {
|
||||
if (CFStringCompare(scheme, CFSTR("http"), 0) == kCFCompareEqualTo) {
|
||||
CFRelease(scheme);
|
||||
// Using cache makes only sense for HTTP
|
||||
return true;
|
||||
}
|
||||
|
||||
CFRelease(scheme);
|
||||
}
|
||||
|
||||
// Nothing else to server
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ID3_Parser_Delegate */
|
||||
void Caching_Stream::id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataAvailable(metaData);
|
||||
}
|
||||
}
|
||||
|
||||
void Caching_Stream::id3tagSizeAvailable(UInt32 tagSize)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataByteSizeAvailable(tagSize);
|
||||
}
|
||||
}
|
||||
|
||||
/* Input_Stream_Delegate */
|
||||
|
||||
void Caching_Stream::streamIsReadyRead()
|
||||
{
|
||||
if (m_cacheable) {
|
||||
// If the stream is cacheable (not seeked from some position)
|
||||
// Check if the stream has a length. If there is no length,
|
||||
// it is a continuous stream and thus cannot be cached.
|
||||
m_cacheable = (m_target->contentLength() > 0);
|
||||
}
|
||||
|
||||
#if CS_DEBUG
|
||||
if (m_cacheable) CS_TRACE("Stream can be cached!\n");
|
||||
else CS_TRACE("Stream cannot be cached\n");
|
||||
#endif
|
||||
|
||||
if (m_delegate) {
|
||||
m_delegate->streamIsReadyRead();
|
||||
}
|
||||
}
|
||||
|
||||
void Caching_Stream::streamHasBytesAvailable(UInt8 *data, UInt32 numBytes)
|
||||
{
|
||||
if (m_cacheable) {
|
||||
if (numBytes > 0) {
|
||||
if (!m_fileOutput) {
|
||||
if (m_fileUrl) {
|
||||
CS_TRACE("Caching started for stream\n");
|
||||
|
||||
m_fileOutput = new File_Output(m_fileUrl);
|
||||
|
||||
m_writable = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_writable && m_fileOutput) {
|
||||
m_writable &= (m_fileOutput->write(data, numBytes) > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_delegate) {
|
||||
m_delegate->streamHasBytesAvailable(data, numBytes);
|
||||
}
|
||||
}
|
||||
|
||||
void Caching_Stream::streamEndEncountered()
|
||||
{
|
||||
if (m_fileOutput) {
|
||||
delete m_fileOutput;
|
||||
m_fileOutput = 0;
|
||||
}
|
||||
|
||||
if (m_cacheable) {
|
||||
if (m_writable) {
|
||||
CS_TRACE("Successfully cached the stream\n");
|
||||
CS_TRACE_CFURL(m_fileUrl);
|
||||
|
||||
// We only write the meta data if the stream was successfully streamed.
|
||||
// In that way we can use the meta data as an indicator that there is a file to stream.
|
||||
|
||||
if (!m_cacheMetaDataWritten) {
|
||||
CFWriteStreamRef writeStream = CFWriteStreamCreateWithFile(kCFAllocatorDefault, m_metaDataUrl);
|
||||
|
||||
if (writeStream) {
|
||||
if (CFWriteStreamOpen(writeStream)) {
|
||||
CFStringRef contentType = m_target->contentType();
|
||||
|
||||
UInt8 buf[1024];
|
||||
CFIndex usedBytes = 0;
|
||||
|
||||
if (contentType) {
|
||||
// It is possible that some streams don't provide a content type
|
||||
CFStringGetBytes(contentType,
|
||||
CFRangeMake(0, CFStringGetLength(contentType)),
|
||||
kCFStringEncodingUTF8,
|
||||
'?',
|
||||
false,
|
||||
buf,
|
||||
1024,
|
||||
&usedBytes);
|
||||
}
|
||||
|
||||
if (usedBytes > 0) {
|
||||
CS_TRACE("Writing the meta data\n");
|
||||
CS_TRACE_CFSTRING(contentType);
|
||||
|
||||
CFWriteStreamWrite(writeStream, buf, usedBytes);
|
||||
}
|
||||
|
||||
CFWriteStreamClose(writeStream);
|
||||
}
|
||||
|
||||
CFRelease(writeStream);
|
||||
}
|
||||
|
||||
m_cacheable = false;
|
||||
m_writable = false;
|
||||
m_useCache = true;
|
||||
m_cacheMetaDataWritten = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_delegate) {
|
||||
m_delegate->streamEndEncountered();
|
||||
}
|
||||
}
|
||||
|
||||
void Caching_Stream::streamErrorOccurred(CFStringRef errorDesc)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamErrorOccurred(errorDesc);
|
||||
}
|
||||
}
|
||||
|
||||
void Caching_Stream::streamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataAvailable(metaData);
|
||||
}
|
||||
}
|
||||
|
||||
void Caching_Stream::streamMetaDataByteSizeAvailable(UInt32 sizeInBytes)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataByteSizeAvailable(sizeInBytes);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace astreamer
|
||||
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_CACHING_STREAM_H
|
||||
#define ASTREAMER_CACHING_STREAM_H
|
||||
|
||||
#include "input_stream.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
class File_Output;
|
||||
class File_Stream;
|
||||
|
||||
class Caching_Stream : public Input_Stream, public Input_Stream_Delegate {
|
||||
private:
|
||||
Input_Stream *m_target;
|
||||
File_Output *m_fileOutput;
|
||||
File_Stream *m_fileStream;
|
||||
bool m_cacheable;
|
||||
bool m_writable;
|
||||
bool m_useCache;
|
||||
bool m_cacheMetaDataWritten;
|
||||
CFStringRef m_cacheIdentifier;
|
||||
CFURLRef m_fileUrl;
|
||||
CFURLRef m_metaDataUrl;
|
||||
|
||||
private:
|
||||
CFURLRef createFileURLWithPath(CFStringRef path);
|
||||
|
||||
void readMetaData();
|
||||
|
||||
public:
|
||||
Caching_Stream(Input_Stream *target);
|
||||
virtual ~Caching_Stream();
|
||||
|
||||
Input_Stream_Position position();
|
||||
|
||||
CFStringRef contentType();
|
||||
size_t contentLength();
|
||||
|
||||
bool open();
|
||||
bool open(const Input_Stream_Position& position);
|
||||
void close();
|
||||
|
||||
void setScheduledInRunLoop(bool scheduledInRunLoop);
|
||||
|
||||
void setUrl(CFURLRef url);
|
||||
|
||||
void setCacheIdentifier(CFStringRef cacheIdentifier);
|
||||
|
||||
static bool canHandleUrl(CFURLRef url);
|
||||
|
||||
/* ID3_Parser_Delegate */
|
||||
void id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
|
||||
void id3tagSizeAvailable(UInt32 tagSize);
|
||||
|
||||
void streamIsReadyRead();
|
||||
void streamHasBytesAvailable(UInt8 *data, UInt32 numBytes);
|
||||
void streamEndEncountered();
|
||||
void streamErrorOccurred(CFStringRef errorDesc);
|
||||
void streamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
|
||||
void streamMetaDataByteSizeAvailable(UInt32 sizeInBytes);
|
||||
};
|
||||
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif /* ASTREAMER_CACHING_STREAM_H */
|
||||
@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "file_output.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
File_Output::File_Output(CFURLRef fileURL) :
|
||||
m_writeStream(CFWriteStreamCreateWithFile(kCFAllocatorDefault, fileURL))
|
||||
{
|
||||
CFWriteStreamOpen(m_writeStream);
|
||||
}
|
||||
|
||||
File_Output::~File_Output()
|
||||
{
|
||||
CFWriteStreamClose(m_writeStream);
|
||||
CFRelease(m_writeStream);
|
||||
}
|
||||
|
||||
CFIndex File_Output::write(const UInt8 *buffer, CFIndex bufferLength)
|
||||
{
|
||||
return CFWriteStreamWrite(m_writeStream, buffer, bufferLength);
|
||||
}
|
||||
|
||||
} // namespace astreamer
|
||||
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_FILE_OUTPUT_H
|
||||
#define ASTREAMER_FILE_OUTPUT_H
|
||||
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
class File_Output {
|
||||
private:
|
||||
File_Output(const File_Output&);
|
||||
File_Output& operator=(const File_Output&);
|
||||
|
||||
CFWriteStreamRef m_writeStream;
|
||||
|
||||
public:
|
||||
File_Output(CFURLRef fileURL);
|
||||
~File_Output();
|
||||
|
||||
CFIndex write(const UInt8 *buffer, CFIndex bufferLength);
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_FILE_OUTPUT_H
|
||||
@ -1,405 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "file_stream.h"
|
||||
#include "stream_configuration.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
File_Stream::File_Stream() :
|
||||
m_url(0),
|
||||
m_readStream(0),
|
||||
m_scheduledInRunLoop(false),
|
||||
m_readPending(false),
|
||||
m_fileReadBuffer(0),
|
||||
m_id3Parser(new ID3_Parser()),
|
||||
m_contentType(0)
|
||||
{
|
||||
m_id3Parser->m_delegate = this;
|
||||
}
|
||||
|
||||
File_Stream::~File_Stream()
|
||||
{
|
||||
close();
|
||||
|
||||
if (m_fileReadBuffer) {
|
||||
delete [] m_fileReadBuffer;
|
||||
m_fileReadBuffer = 0;
|
||||
}
|
||||
|
||||
if (m_url) {
|
||||
CFRelease(m_url);
|
||||
m_url = 0;
|
||||
}
|
||||
|
||||
delete m_id3Parser;
|
||||
m_id3Parser = 0;
|
||||
|
||||
if (m_contentType) {
|
||||
CFRelease(m_contentType);
|
||||
}
|
||||
}
|
||||
|
||||
Input_Stream_Position File_Stream::position()
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
|
||||
CFStringRef File_Stream::contentType()
|
||||
{
|
||||
if (m_contentType) {
|
||||
// Use the provided content type
|
||||
return m_contentType;
|
||||
}
|
||||
|
||||
// Try to resolve the content type from the file
|
||||
|
||||
CFStringRef contentType = CFSTR("");
|
||||
CFStringRef pathComponent = 0;
|
||||
CFIndex len = 0;
|
||||
CFRange range;
|
||||
CFStringRef suffix = 0;
|
||||
|
||||
if (!m_url) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
pathComponent = CFURLCopyLastPathComponent(m_url);
|
||||
|
||||
if (!pathComponent) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
len = CFStringGetLength(pathComponent);
|
||||
|
||||
if (len > 5) {
|
||||
range.length = 4;
|
||||
range.location = len - 4;
|
||||
|
||||
suffix = CFStringCreateWithSubstring(kCFAllocatorDefault,
|
||||
pathComponent,
|
||||
range);
|
||||
|
||||
if (!suffix) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
// TODO: we should do the content-type resolvation in a better way.
|
||||
if (CFStringCompare(suffix, CFSTR(".mp3"), 0) == kCFCompareEqualTo) {
|
||||
contentType = CFSTR("audio/mpeg");
|
||||
} else if (CFStringCompare(suffix, CFSTR(".m4a"), 0) == kCFCompareEqualTo) {
|
||||
contentType = CFSTR("audio/x-m4a");
|
||||
} else if (CFStringCompare(suffix, CFSTR(".mp4"), 0) == kCFCompareEqualTo) {
|
||||
contentType = CFSTR("audio/mp4");
|
||||
} else if (CFStringCompare(suffix, CFSTR(".aac"), 0) == kCFCompareEqualTo) {
|
||||
contentType = CFSTR("audio/aac");
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
if (pathComponent) {
|
||||
CFRelease(pathComponent);
|
||||
}
|
||||
if (suffix) {
|
||||
CFRelease(suffix);
|
||||
}
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
void File_Stream::setContentType(CFStringRef contentType)
|
||||
{
|
||||
if (m_contentType) {
|
||||
CFRelease(m_contentType);
|
||||
m_contentType = 0;
|
||||
}
|
||||
if (contentType) {
|
||||
m_contentType = CFStringCreateCopy(kCFAllocatorDefault, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
size_t File_Stream::contentLength()
|
||||
{
|
||||
CFNumberRef length = NULL;
|
||||
CFErrorRef err = NULL;
|
||||
|
||||
if (CFURLCopyResourcePropertyForKey(m_url, kCFURLFileSizeKey, &length, &err)) {
|
||||
CFIndex fileLength;
|
||||
if (CFNumberGetValue(length, kCFNumberCFIndexType, &fileLength)) {
|
||||
CFRelease(length);
|
||||
|
||||
return fileLength;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool File_Stream::open()
|
||||
{
|
||||
Input_Stream_Position position;
|
||||
position.start = 0;
|
||||
position.end = 0;
|
||||
|
||||
m_id3Parser->reset();
|
||||
|
||||
return open(position);
|
||||
}
|
||||
|
||||
bool File_Stream::open(const Input_Stream_Position& position)
|
||||
{
|
||||
bool success = false;
|
||||
CFStreamClientContext CTX = { 0, this, NULL, NULL, NULL };
|
||||
|
||||
/* Already opened a read stream, return */
|
||||
if (m_readStream) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!m_url) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Reset state */
|
||||
m_position = position;
|
||||
|
||||
m_readPending = false;
|
||||
|
||||
/* Failed to create a stream */
|
||||
if (!(m_readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, m_url))) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (m_position.start > 0) {
|
||||
CFNumberRef position = CFNumberCreate(0, kCFNumberLongLongType, &m_position.start);
|
||||
CFReadStreamSetProperty(m_readStream, kCFStreamPropertyFileCurrentOffset, position);
|
||||
CFRelease(position);
|
||||
}
|
||||
|
||||
if (!CFReadStreamSetClient(m_readStream, kCFStreamEventHasBytesAvailable |
|
||||
kCFStreamEventEndEncountered |
|
||||
kCFStreamEventErrorOccurred, readCallBack, &CTX)) {
|
||||
CFRelease(m_readStream);
|
||||
m_readStream = 0;
|
||||
goto out;
|
||||
}
|
||||
|
||||
setScheduledInRunLoop(true);
|
||||
|
||||
if (!CFReadStreamOpen(m_readStream)) {
|
||||
/* Open failed: clean */
|
||||
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
|
||||
setScheduledInRunLoop(false);
|
||||
if (m_readStream) {
|
||||
CFRelease(m_readStream);
|
||||
m_readStream = 0;
|
||||
}
|
||||
goto out;
|
||||
}
|
||||
|
||||
success = true;
|
||||
|
||||
out:
|
||||
|
||||
if (success) {
|
||||
if (m_delegate) {
|
||||
m_delegate->streamIsReadyRead();
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void File_Stream::close()
|
||||
{
|
||||
/* The stream has been already closed */
|
||||
if (!m_readStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
|
||||
setScheduledInRunLoop(false);
|
||||
CFReadStreamClose(m_readStream);
|
||||
CFRelease(m_readStream);
|
||||
m_readStream = 0;
|
||||
}
|
||||
|
||||
void File_Stream::setScheduledInRunLoop(bool scheduledInRunLoop)
|
||||
{
|
||||
/* The stream has not been opened, or it has been already closed */
|
||||
if (!m_readStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* The state doesn't change */
|
||||
if (m_scheduledInRunLoop == scheduledInRunLoop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_scheduledInRunLoop) {
|
||||
CFReadStreamUnscheduleFromRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
|
||||
} else {
|
||||
if (m_readPending) {
|
||||
m_readPending = false;
|
||||
|
||||
readCallBack(m_readStream, kCFStreamEventHasBytesAvailable, this);
|
||||
}
|
||||
|
||||
CFReadStreamScheduleWithRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
|
||||
}
|
||||
|
||||
m_scheduledInRunLoop = scheduledInRunLoop;
|
||||
}
|
||||
|
||||
void File_Stream::setUrl(CFURLRef url)
|
||||
{
|
||||
if (m_url) {
|
||||
CFRelease(m_url);
|
||||
}
|
||||
if (url) {
|
||||
m_url = (CFURLRef)CFRetain(url);
|
||||
} else {
|
||||
m_url = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bool File_Stream::canHandleUrl(CFURLRef url)
|
||||
{
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CFStringRef scheme = CFURLCopyScheme(url);
|
||||
|
||||
if (scheme) {
|
||||
if (CFStringCompare(scheme, CFSTR("file"), 0) == kCFCompareEqualTo) {
|
||||
CFRelease(scheme);
|
||||
// The only scheme we claim to handle are the local files
|
||||
return true;
|
||||
}
|
||||
|
||||
CFRelease(scheme);
|
||||
}
|
||||
|
||||
// We don't handle anything else but local files
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ID3_Parser_Delegate */
|
||||
void File_Stream::id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataAvailable(metaData);
|
||||
}
|
||||
}
|
||||
|
||||
void File_Stream::id3tagSizeAvailable(UInt32 tagSize)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataByteSizeAvailable(tagSize);
|
||||
}
|
||||
}
|
||||
|
||||
void File_Stream::readCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo)
|
||||
{
|
||||
File_Stream *THIS = static_cast<File_Stream*>(clientCallBackInfo);
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
switch (eventType) {
|
||||
case kCFStreamEventHasBytesAvailable: {
|
||||
if (!THIS->m_fileReadBuffer) {
|
||||
THIS->m_fileReadBuffer = new UInt8[config->httpConnectionBufferSize];
|
||||
}
|
||||
|
||||
while (CFReadStreamHasBytesAvailable(stream)) {
|
||||
if (!THIS->m_scheduledInRunLoop) {
|
||||
/*
|
||||
* This is critical - though the stream has data available,
|
||||
* do not try to feed the audio queue with data, if it has
|
||||
* indicated that it doesn't want more data due to buffers
|
||||
* full.
|
||||
*/
|
||||
THIS->m_readPending = true;
|
||||
break;
|
||||
}
|
||||
|
||||
CFIndex bytesRead = CFReadStreamRead(stream, THIS->m_fileReadBuffer, config->httpConnectionBufferSize);
|
||||
|
||||
if (CFReadStreamGetStatus(stream) == kCFStreamStatusError ||
|
||||
bytesRead < 0) {
|
||||
|
||||
if (THIS->m_delegate) {
|
||||
CFStringRef reportedNetworkError = NULL;
|
||||
CFErrorRef streamError = CFReadStreamCopyError(stream);
|
||||
|
||||
if (streamError) {
|
||||
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
|
||||
|
||||
if (errorDesc) {
|
||||
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
|
||||
|
||||
CFRelease(errorDesc);
|
||||
}
|
||||
|
||||
CFRelease(streamError);
|
||||
}
|
||||
|
||||
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
|
||||
if (reportedNetworkError) {
|
||||
CFRelease(reportedNetworkError);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytesRead > 0) {
|
||||
if (THIS->m_delegate) {
|
||||
THIS->m_delegate->streamHasBytesAvailable(THIS->m_fileReadBuffer, (UInt32)bytesRead);
|
||||
}
|
||||
|
||||
if (THIS->m_id3Parser->wantData()) {
|
||||
THIS->m_id3Parser->feedData(THIS->m_fileReadBuffer, (UInt32)bytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case kCFStreamEventEndEncountered: {
|
||||
if (THIS->m_delegate) {
|
||||
THIS->m_delegate->streamEndEncountered();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kCFStreamEventErrorOccurred: {
|
||||
if (THIS->m_delegate) {
|
||||
CFStringRef reportedNetworkError = NULL;
|
||||
CFErrorRef streamError = CFReadStreamCopyError(stream);
|
||||
|
||||
if (streamError) {
|
||||
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
|
||||
|
||||
if (errorDesc) {
|
||||
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
|
||||
|
||||
CFRelease(errorDesc);
|
||||
}
|
||||
|
||||
CFRelease(streamError);
|
||||
}
|
||||
|
||||
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
|
||||
if (reportedNetworkError) {
|
||||
CFRelease(reportedNetworkError);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace astreamer
|
||||
@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_FILE_STREAM_H
|
||||
#define ASTREAMER_FILE_STREAM_H
|
||||
|
||||
#import "input_stream.h"
|
||||
#import "id3_parser.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
class File_Stream : public Input_Stream {
|
||||
private:
|
||||
|
||||
File_Stream(const File_Stream&);
|
||||
File_Stream& operator=(const File_Stream&);
|
||||
|
||||
CFURLRef m_url;
|
||||
CFReadStreamRef m_readStream;
|
||||
bool m_scheduledInRunLoop;
|
||||
bool m_readPending;
|
||||
Input_Stream_Position m_position;
|
||||
|
||||
UInt8 *m_fileReadBuffer;
|
||||
|
||||
ID3_Parser *m_id3Parser;
|
||||
|
||||
CFStringRef m_contentType;
|
||||
|
||||
static void readCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo);
|
||||
|
||||
public:
|
||||
File_Stream();
|
||||
virtual ~File_Stream();
|
||||
|
||||
Input_Stream_Position position();
|
||||
|
||||
CFStringRef contentType();
|
||||
void setContentType(CFStringRef contentType);
|
||||
size_t contentLength();
|
||||
|
||||
bool open();
|
||||
bool open(const Input_Stream_Position& position);
|
||||
void close();
|
||||
|
||||
void setScheduledInRunLoop(bool scheduledInRunLoop);
|
||||
|
||||
void setUrl(CFURLRef url);
|
||||
|
||||
static bool canHandleUrl(CFURLRef url);
|
||||
|
||||
/* ID3_Parser_Delegate */
|
||||
void id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
|
||||
void id3tagSizeAvailable(UInt32 tagSize);
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_FILE_STREAM_H
|
||||
@ -1,908 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "http_stream.h"
|
||||
#include "audio_queue.h"
|
||||
#include "id3_parser.h"
|
||||
#include "stream_configuration.h"
|
||||
|
||||
//#define HS_DEBUG 1
|
||||
|
||||
#if !defined (HS_DEBUG)
|
||||
#define HS_TRACE(...) do {} while (0)
|
||||
#define HS_TRACE_CFSTRING(X) do {} while (0)
|
||||
#else
|
||||
#define HS_TRACE(...) printf(__VA_ARGS__)
|
||||
#define HS_TRACE_CFSTRING(X) HS_TRACE("%s\n", CFStringGetCStringPtr(X, kCFStringEncodingMacRoman))
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Comment the following line to disable ID3 tag support:
|
||||
*/
|
||||
#define INCLUDE_ID3TAG_SUPPORT 1
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
CFStringRef HTTP_Stream::httpRequestMethod = CFSTR("GET");
|
||||
CFStringRef HTTP_Stream::httpUserAgentHeader = CFSTR("User-Agent");
|
||||
CFStringRef HTTP_Stream::httpRangeHeader = CFSTR("Range");
|
||||
CFStringRef HTTP_Stream::icyMetaDataHeader = CFSTR("Icy-MetaData");
|
||||
CFStringRef HTTP_Stream::icyMetaDataValue = CFSTR("1"); /* always request ICY metadata, if available */
|
||||
|
||||
|
||||
/* HTTP_Stream: public */
|
||||
HTTP_Stream::HTTP_Stream() :
|
||||
m_readStream(0),
|
||||
m_scheduledInRunLoop(false),
|
||||
m_readPending(false),
|
||||
m_url(0),
|
||||
m_httpHeadersParsed(false),
|
||||
m_contentType(0),
|
||||
m_contentLength(0),
|
||||
m_bytesRead(0),
|
||||
|
||||
m_icyStream(false),
|
||||
m_icyHeaderCR(false),
|
||||
m_icyHeadersRead(false),
|
||||
m_icyHeadersParsed(false),
|
||||
|
||||
m_icyName(0),
|
||||
|
||||
m_icyMetaDataInterval(0),
|
||||
m_dataByteReadCount(0),
|
||||
m_metaDataBytesRemaining(0),
|
||||
|
||||
m_httpReadBuffer(0),
|
||||
m_icyReadBuffer(0),
|
||||
|
||||
m_id3Parser(new ID3_Parser())
|
||||
{
|
||||
m_id3Parser->m_delegate = this;
|
||||
}
|
||||
|
||||
HTTP_Stream::~HTTP_Stream()
|
||||
{
|
||||
close();
|
||||
|
||||
for (std::vector<CFStringRef>::iterator h = m_icyHeaderLines.begin(); h != m_icyHeaderLines.end(); ++h) {
|
||||
CFRelease(*h);
|
||||
}
|
||||
|
||||
m_icyHeaderLines.clear();
|
||||
|
||||
if (m_contentType) {
|
||||
CFRelease(m_contentType);
|
||||
m_contentType = 0;
|
||||
}
|
||||
|
||||
if (m_icyName) {
|
||||
CFRelease(m_icyName);
|
||||
m_icyName = 0;
|
||||
}
|
||||
|
||||
if (m_httpReadBuffer) {
|
||||
delete [] m_httpReadBuffer;
|
||||
m_httpReadBuffer = 0;
|
||||
}
|
||||
if (m_icyReadBuffer) {
|
||||
delete [] m_icyReadBuffer;
|
||||
m_icyReadBuffer = 0;
|
||||
}
|
||||
if (m_url) {
|
||||
CFRelease(m_url);
|
||||
m_url = 0;
|
||||
}
|
||||
|
||||
delete m_id3Parser;
|
||||
m_id3Parser = 0;
|
||||
}
|
||||
|
||||
Input_Stream_Position HTTP_Stream::position()
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
|
||||
CFStringRef HTTP_Stream::contentType()
|
||||
{
|
||||
return m_contentType;
|
||||
}
|
||||
|
||||
size_t HTTP_Stream::contentLength()
|
||||
{
|
||||
return m_contentLength;
|
||||
}
|
||||
|
||||
bool HTTP_Stream::open()
|
||||
{
|
||||
Input_Stream_Position position;
|
||||
position.start = 0;
|
||||
position.end = 0;
|
||||
|
||||
m_contentLength = 0;
|
||||
#ifdef INCLUDE_ID3TAG_SUPPORT
|
||||
m_id3Parser->reset();
|
||||
#endif
|
||||
|
||||
return open(position);
|
||||
}
|
||||
|
||||
bool HTTP_Stream::open(const Input_Stream_Position& position)
|
||||
{
|
||||
bool success = false;
|
||||
CFStreamClientContext CTX = { 0, this, NULL, NULL, NULL };
|
||||
|
||||
/* Already opened a read stream, return */
|
||||
if (m_readStream) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Reset state */
|
||||
m_position = position;
|
||||
|
||||
m_readPending = false;
|
||||
m_httpHeadersParsed = false;
|
||||
|
||||
if (m_contentType) {
|
||||
CFRelease(m_contentType);
|
||||
m_contentType = NULL;
|
||||
}
|
||||
|
||||
m_icyStream = false;
|
||||
m_icyHeaderCR = false;
|
||||
m_icyHeadersRead = false;
|
||||
m_icyHeadersParsed = false;
|
||||
|
||||
if (m_icyName) {
|
||||
CFRelease(m_icyName);
|
||||
m_icyName = 0;
|
||||
}
|
||||
|
||||
for (std::vector<CFStringRef>::iterator h = m_icyHeaderLines.begin(); h != m_icyHeaderLines.end(); ++h) {
|
||||
CFRelease(*h);
|
||||
}
|
||||
|
||||
m_icyHeaderLines.clear();
|
||||
m_icyMetaDataInterval = 0;
|
||||
m_dataByteReadCount = 0;
|
||||
m_metaDataBytesRemaining = 0;
|
||||
m_bytesRead = 0;
|
||||
|
||||
if (!m_url) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Failed to create a stream */
|
||||
if (!(m_readStream = createReadStream(m_url))) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!CFReadStreamSetClient(m_readStream, kCFStreamEventHasBytesAvailable |
|
||||
kCFStreamEventEndEncountered |
|
||||
kCFStreamEventErrorOccurred, readCallBack, &CTX)) {
|
||||
CFRelease(m_readStream);
|
||||
m_readStream = 0;
|
||||
goto out;
|
||||
}
|
||||
|
||||
setScheduledInRunLoop(true);
|
||||
|
||||
if (!CFReadStreamOpen(m_readStream)) {
|
||||
/* Open failed: clean */
|
||||
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
|
||||
setScheduledInRunLoop(false);
|
||||
if (m_readStream) {
|
||||
CFRelease(m_readStream);
|
||||
m_readStream = 0;
|
||||
}
|
||||
goto out;
|
||||
}
|
||||
|
||||
success = true;
|
||||
|
||||
out:
|
||||
return success;
|
||||
}
|
||||
|
||||
void HTTP_Stream::close()
|
||||
{
|
||||
/* The stream has been already closed */
|
||||
if (!m_readStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
|
||||
setScheduledInRunLoop(false);
|
||||
CFReadStreamClose(m_readStream);
|
||||
CFRelease(m_readStream);
|
||||
m_readStream = 0;
|
||||
}
|
||||
|
||||
void HTTP_Stream::setScheduledInRunLoop(bool scheduledInRunLoop)
|
||||
{
|
||||
/* The stream has not been opened, or it has been already closed */
|
||||
if (!m_readStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* The state doesn't change */
|
||||
if (m_scheduledInRunLoop == scheduledInRunLoop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_scheduledInRunLoop) {
|
||||
CFReadStreamUnscheduleFromRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
|
||||
} else {
|
||||
if (m_readPending) {
|
||||
m_readPending = false;
|
||||
|
||||
readCallBack(m_readStream, kCFStreamEventHasBytesAvailable, this);
|
||||
}
|
||||
|
||||
CFReadStreamScheduleWithRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
|
||||
}
|
||||
|
||||
m_scheduledInRunLoop = scheduledInRunLoop;
|
||||
}
|
||||
|
||||
void HTTP_Stream::setUrl(CFURLRef url)
|
||||
{
|
||||
if (m_url) {
|
||||
CFRelease(m_url);
|
||||
}
|
||||
if (url) {
|
||||
m_url = (CFURLRef)CFRetain(url);
|
||||
} else {
|
||||
m_url = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bool HTTP_Stream::canHandleUrl(CFURLRef url)
|
||||
{
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CFStringRef scheme = CFURLCopyScheme(url);
|
||||
|
||||
if (scheme) {
|
||||
if (CFStringCompare(scheme, CFSTR("file"), 0) == kCFCompareEqualTo) {
|
||||
CFRelease(scheme);
|
||||
|
||||
// The only scheme we claim not to handle are local files.
|
||||
return false;
|
||||
}
|
||||
|
||||
CFRelease(scheme);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void HTTP_Stream::id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataAvailable(metaData);
|
||||
}
|
||||
}
|
||||
|
||||
void HTTP_Stream::id3tagSizeAvailable(UInt32 tagSize)
|
||||
{
|
||||
if (m_delegate) {
|
||||
m_delegate->streamMetaDataByteSizeAvailable(tagSize);
|
||||
}
|
||||
}
|
||||
|
||||
/* private */
|
||||
|
||||
CFReadStreamRef HTTP_Stream::createReadStream(CFURLRef url)
|
||||
{
|
||||
CFReadStreamRef readStream = 0;
|
||||
CFHTTPMessageRef request = 0;
|
||||
CFDictionaryRef proxySettings = 0;
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
if (!(request = CFHTTPMessageCreateRequest(kCFAllocatorDefault, httpRequestMethod, url, kCFHTTPVersion1_1))) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (config->userAgent) {
|
||||
CFHTTPMessageSetHeaderFieldValue(request, httpUserAgentHeader, config->userAgent);
|
||||
}
|
||||
|
||||
CFHTTPMessageSetHeaderFieldValue(request, icyMetaDataHeader, icyMetaDataValue);
|
||||
|
||||
if (m_position.start > 0 && m_position.end > m_position.start) {
|
||||
CFStringRef rangeHeaderValue = CFStringCreateWithFormat(NULL,
|
||||
NULL,
|
||||
CFSTR("bytes=%llu-%llu"),
|
||||
m_position.start,
|
||||
m_position.end);
|
||||
|
||||
CFHTTPMessageSetHeaderFieldValue(request, httpRangeHeader, rangeHeaderValue);
|
||||
CFRelease(rangeHeaderValue);
|
||||
} else if (m_position.start > 0 && m_position.end < m_position.start) {
|
||||
CFStringRef rangeHeaderValue = CFStringCreateWithFormat(NULL,
|
||||
NULL,
|
||||
CFSTR("bytes=%llu-"),
|
||||
m_position.start);
|
||||
CFHTTPMessageSetHeaderFieldValue(request, httpRangeHeader, rangeHeaderValue);
|
||||
CFRelease(rangeHeaderValue);
|
||||
}
|
||||
|
||||
|
||||
if (config->predefinedHttpHeaderValues) {
|
||||
const CFIndex numKeys = CFDictionaryGetCount(config->predefinedHttpHeaderValues);
|
||||
|
||||
if (numKeys > 0) {
|
||||
CFTypeRef *keys = (CFTypeRef *) malloc(numKeys * sizeof(CFTypeRef));
|
||||
|
||||
if (keys) {
|
||||
CFDictionaryGetKeysAndValues(config->predefinedHttpHeaderValues, (const void **) keys, NULL);
|
||||
|
||||
for (CFIndex i=0; i < numKeys; i++) {
|
||||
CFTypeRef key = keys[i];
|
||||
|
||||
if (CFGetTypeID(key) == CFStringGetTypeID()) {
|
||||
const void *value = CFDictionaryGetValue(config->predefinedHttpHeaderValues, (const void *) key);
|
||||
|
||||
if (value) {
|
||||
CFStringRef headerKey = (CFStringRef) key;
|
||||
|
||||
CFTypeRef valueRef = (CFTypeRef) value;
|
||||
|
||||
if (CFGetTypeID(valueRef) == CFStringGetTypeID()) {
|
||||
CFStringRef headerValue = (CFStringRef) valueRef;
|
||||
|
||||
HS_TRACE("Setting predefined HTTP header ");
|
||||
HS_TRACE_CFSTRING(headerKey);
|
||||
HS_TRACE_CFSTRING(headerValue);
|
||||
|
||||
CFHTTPMessageSetHeaderFieldValue(request, headerKey, headerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request))) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
CFReadStreamSetProperty(readStream,
|
||||
kCFStreamNetworkServiceType,
|
||||
kCFStreamNetworkServiceTypeBackground);
|
||||
|
||||
CFReadStreamSetProperty(readStream,
|
||||
kCFStreamPropertyHTTPShouldAutoredirect,
|
||||
kCFBooleanTrue);
|
||||
|
||||
proxySettings = CFNetworkCopySystemProxySettings();
|
||||
if (proxySettings) {
|
||||
CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, proxySettings);
|
||||
CFRelease(proxySettings);
|
||||
}
|
||||
|
||||
out:
|
||||
if (request) {
|
||||
CFRelease(request);
|
||||
}
|
||||
|
||||
return readStream;
|
||||
}
|
||||
|
||||
void HTTP_Stream::parseHttpHeadersIfNeeded(const UInt8 *buf, const CFIndex bufSize)
|
||||
{
|
||||
if (m_httpHeadersParsed) {
|
||||
return;
|
||||
}
|
||||
m_httpHeadersParsed = true;
|
||||
|
||||
/* If the response has the "ICY 200 OK" string,
|
||||
* we are dealing with the ShoutCast protocol.
|
||||
* The HTTP headers won't be available.
|
||||
*/
|
||||
if (bufSize >= 10 &&
|
||||
buf[0] == 0x49 && buf[1] == 0x43 && buf[2] == 0x59 &&
|
||||
buf[3] == 0x20 && buf[4] == 0x32 && buf[5] == 0x30 &&
|
||||
buf[6] == 0x30 && buf[7] == 0x20 && buf[8] == 0x4F &&
|
||||
buf[9] == 0x4B) {
|
||||
m_icyStream = true;
|
||||
|
||||
HS_TRACE("Detected an IceCast stream\n");
|
||||
|
||||
// This is an ICY stream, don't try to parse the HTTP headers
|
||||
return;
|
||||
}
|
||||
|
||||
HS_TRACE("A regular HTTP stream\n");
|
||||
|
||||
CFHTTPMessageRef response = (CFHTTPMessageRef)CFReadStreamCopyProperty(m_readStream, kCFStreamPropertyHTTPResponseHeader);
|
||||
CFIndex statusCode = 0;
|
||||
|
||||
if (response) {
|
||||
/*
|
||||
* If the server responded with the icy-metaint header, the response
|
||||
* body will be encoded in the ShoutCast protocol.
|
||||
*/
|
||||
CFStringRef icyMetaIntString = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("icy-metaint"));
|
||||
if (icyMetaIntString) {
|
||||
m_icyStream = true;
|
||||
m_icyHeadersParsed = true;
|
||||
m_icyHeadersRead = true;
|
||||
m_icyMetaDataInterval = CFStringGetIntValue(icyMetaIntString);
|
||||
CFRelease(icyMetaIntString);
|
||||
}
|
||||
|
||||
HS_TRACE("icy-metaint: %zu\n", m_icyMetaDataInterval);
|
||||
|
||||
statusCode = CFHTTPMessageGetResponseStatusCode(response);
|
||||
|
||||
HS_TRACE("HTTP response code %zu", statusCode);
|
||||
|
||||
CFStringRef icyNameString = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("icy-name"));
|
||||
if (icyNameString) {
|
||||
if (m_icyName) {
|
||||
CFRelease(m_icyName);
|
||||
}
|
||||
m_icyName = icyNameString;
|
||||
|
||||
if (m_delegate) {
|
||||
std::map<CFStringRef,CFStringRef> metadataMap;
|
||||
|
||||
metadataMap[CFSTR("IcecastStationName")] = CFStringCreateCopy(kCFAllocatorDefault, m_icyName);
|
||||
|
||||
m_delegate->streamMetaDataAvailable(metadataMap);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_contentType) {
|
||||
CFRelease(m_contentType);
|
||||
}
|
||||
|
||||
m_contentType = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("Content-Type"));
|
||||
|
||||
HS_TRACE("Content-type: ");
|
||||
HS_TRACE_CFSTRING(m_contentType);
|
||||
|
||||
CFStringRef contentLengthString = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("Content-Length"));
|
||||
if (contentLengthString) {
|
||||
m_contentLength = CFStringGetIntValue(contentLengthString);
|
||||
|
||||
CFRelease(contentLengthString);
|
||||
}
|
||||
|
||||
CFRelease(response);
|
||||
}
|
||||
|
||||
if (m_delegate &&
|
||||
(statusCode == 200 || statusCode == 206)) {
|
||||
m_delegate->streamIsReadyRead();
|
||||
} else {
|
||||
if (m_delegate) {
|
||||
CFStringRef statusCodeString = CFStringCreateWithFormat(NULL,
|
||||
NULL,
|
||||
CFSTR("HTTP response code %d"),
|
||||
(unsigned int)statusCode);
|
||||
m_delegate->streamErrorOccurred(statusCodeString);
|
||||
|
||||
if (statusCodeString) {
|
||||
CFRelease(statusCodeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HTTP_Stream::parseICYStream(const UInt8 *buf, const CFIndex bufSize)
|
||||
{
|
||||
HS_TRACE("Parsing an IceCast stream, received %li bytes\n", bufSize);
|
||||
|
||||
CFIndex offset = 0;
|
||||
CFIndex bytesFound = 0;
|
||||
if (!m_icyHeadersRead) {
|
||||
HS_TRACE("ICY headers not read, reading\n");
|
||||
|
||||
for (; offset < bufSize; offset++) {
|
||||
if (m_icyHeaderCR && buf[offset] == '\n') {
|
||||
if (bytesFound > 0) {
|
||||
m_icyHeaderLines.push_back(createMetaDataStringWithMostReasonableEncoding(&buf[offset-bytesFound-1], bytesFound));
|
||||
|
||||
bytesFound = 0;
|
||||
|
||||
HS_TRACE_CFSTRING(m_icyHeaderLines[m_icyHeaderLines.size()-1]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
HS_TRACE("End of ICY headers\n");
|
||||
|
||||
m_icyHeadersRead = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (buf[offset] == '\r') {
|
||||
m_icyHeaderCR = true;
|
||||
continue;
|
||||
} else {
|
||||
m_icyHeaderCR = false;
|
||||
}
|
||||
|
||||
bytesFound++;
|
||||
}
|
||||
} else if (!m_icyHeadersParsed) {
|
||||
HS_TRACE("ICY headers not parsed, parsing\n");
|
||||
|
||||
const CFStringRef icyContentTypeHeader = CFSTR("content-type:");
|
||||
const CFStringRef icyMetaDataHeader = CFSTR("icy-metaint:");
|
||||
const CFStringRef icyNameHeader = CFSTR("icy-name:");
|
||||
|
||||
const CFIndex icyContenTypeHeaderLength = CFStringGetLength(icyContentTypeHeader);
|
||||
const CFIndex icyMetaDataHeaderLength = CFStringGetLength(icyMetaDataHeader);
|
||||
const CFIndex icyNameHeaderLength = CFStringGetLength(icyNameHeader);
|
||||
|
||||
for (std::vector<CFStringRef>::iterator h = m_icyHeaderLines.begin(); h != m_icyHeaderLines.end(); ++h) {
|
||||
CFStringRef line = *h;
|
||||
const CFIndex lineLength = CFStringGetLength(line);
|
||||
|
||||
if (lineLength == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HS_TRACE_CFSTRING(line);
|
||||
|
||||
if (CFStringCompareWithOptions(line,
|
||||
icyContentTypeHeader,
|
||||
CFRangeMake(0, icyContenTypeHeaderLength),
|
||||
0) == kCFCompareEqualTo) {
|
||||
if (m_contentType) {
|
||||
CFRelease(m_contentType);
|
||||
m_contentType = 0;
|
||||
}
|
||||
m_contentType = CFStringCreateWithSubstring(kCFAllocatorDefault,
|
||||
line,
|
||||
CFRangeMake(icyContenTypeHeaderLength, lineLength - icyContenTypeHeaderLength));
|
||||
|
||||
}
|
||||
|
||||
if (CFStringCompareWithOptions(line,
|
||||
icyMetaDataHeader,
|
||||
CFRangeMake(0, icyMetaDataHeaderLength),
|
||||
0) == kCFCompareEqualTo) {
|
||||
CFStringRef metadataInterval = CFStringCreateWithSubstring(kCFAllocatorDefault,
|
||||
line,
|
||||
CFRangeMake(icyMetaDataHeaderLength, lineLength - icyMetaDataHeaderLength));
|
||||
|
||||
if (metadataInterval) {
|
||||
m_icyMetaDataInterval = CFStringGetIntValue(metadataInterval);
|
||||
|
||||
CFRelease(metadataInterval);
|
||||
} else {
|
||||
m_icyMetaDataInterval = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (CFStringCompareWithOptions(line,
|
||||
icyNameHeader,
|
||||
CFRangeMake(0, icyNameHeaderLength),
|
||||
0) == kCFCompareEqualTo) {
|
||||
if (m_icyName) {
|
||||
CFRelease(m_icyName);
|
||||
}
|
||||
|
||||
m_icyName = CFStringCreateWithSubstring(kCFAllocatorDefault,
|
||||
line,
|
||||
CFRangeMake(icyNameHeaderLength, lineLength - icyNameHeaderLength));
|
||||
}
|
||||
}
|
||||
|
||||
m_icyHeadersParsed = true;
|
||||
offset++;
|
||||
|
||||
if (m_delegate) {
|
||||
m_delegate->streamIsReadyRead();
|
||||
}
|
||||
}
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
if (!m_icyReadBuffer) {
|
||||
m_icyReadBuffer = new UInt8[config->httpConnectionBufferSize];
|
||||
}
|
||||
|
||||
HS_TRACE("Reading ICY stream for playback\n");
|
||||
|
||||
UInt32 i=0;
|
||||
|
||||
for (; offset < bufSize; offset++) {
|
||||
// is this a metadata byte?
|
||||
if (m_metaDataBytesRemaining > 0) {
|
||||
m_metaDataBytesRemaining--;
|
||||
|
||||
if (m_metaDataBytesRemaining == 0) {
|
||||
m_dataByteReadCount = 0;
|
||||
|
||||
if (m_delegate && !m_icyMetaData.empty()) {
|
||||
std::map<CFStringRef,CFStringRef> metadataMap;
|
||||
|
||||
CFStringRef metaData = createMetaDataStringWithMostReasonableEncoding(&m_icyMetaData[0],
|
||||
m_icyMetaData.size());
|
||||
|
||||
if (!metaData) {
|
||||
// Metadata encoding failed, cannot parse.
|
||||
m_icyMetaData.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
CFArrayRef tokens = CFStringCreateArrayBySeparatingStrings(kCFAllocatorDefault,
|
||||
metaData,
|
||||
CFSTR(";"));
|
||||
|
||||
for (CFIndex i=0, max=CFArrayGetCount(tokens); i < max; i++) {
|
||||
CFStringRef token = (CFStringRef) CFArrayGetValueAtIndex(tokens, i);
|
||||
|
||||
CFRange foundRange;
|
||||
|
||||
if (CFStringFindWithOptions(token,
|
||||
CFSTR("='"),
|
||||
CFRangeMake(0, CFStringGetLength(token)),
|
||||
NULL,
|
||||
&foundRange) == true) {
|
||||
|
||||
CFRange keyRange = CFRangeMake(0, foundRange.location);
|
||||
|
||||
CFStringRef metadaKey = CFStringCreateWithSubstring(kCFAllocatorDefault,
|
||||
token,
|
||||
keyRange);
|
||||
|
||||
CFRange valueRange = CFRangeMake(foundRange.location + 2, CFStringGetLength(token) - keyRange.length - 3);
|
||||
|
||||
CFStringRef metadaValue = CFStringCreateWithSubstring(kCFAllocatorDefault,
|
||||
token,
|
||||
valueRange);
|
||||
|
||||
metadataMap[metadaKey] = metadaValue;
|
||||
}
|
||||
}
|
||||
|
||||
CFRelease(tokens);
|
||||
CFRelease(metaData);
|
||||
|
||||
if (m_icyName) {
|
||||
metadataMap[CFSTR("IcecastStationName")] = CFStringCreateCopy(kCFAllocatorDefault, m_icyName);
|
||||
}
|
||||
|
||||
m_delegate->streamMetaDataAvailable(metadataMap);
|
||||
}
|
||||
m_icyMetaData.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
m_icyMetaData.push_back(buf[offset]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// is this the interval byte?
|
||||
if (m_icyMetaDataInterval > 0 && m_dataByteReadCount == m_icyMetaDataInterval) {
|
||||
m_metaDataBytesRemaining = buf[offset] * 16;
|
||||
|
||||
if (m_metaDataBytesRemaining == 0) {
|
||||
m_dataByteReadCount = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// a data byte
|
||||
m_dataByteReadCount++;
|
||||
m_icyReadBuffer[i++] = buf[offset];
|
||||
}
|
||||
|
||||
if (m_delegate && i > 0) {
|
||||
m_delegate->streamHasBytesAvailable(m_icyReadBuffer, i);
|
||||
}
|
||||
}
|
||||
|
||||
#define TRY_ENCODING(STR,ENC) STR = CFStringCreateWithBytes(kCFAllocatorDefault, bytes, numBytes, ENC, false); \
|
||||
if (STR != NULL) { return STR; }
|
||||
|
||||
CFStringRef HTTP_Stream::createMetaDataStringWithMostReasonableEncoding(const UInt8 *bytes, const CFIndex numBytes)
|
||||
{
|
||||
CFStringRef metaData;
|
||||
|
||||
TRY_ENCODING(metaData, kCFStringEncodingUTF8);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin1);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingWindowsLatin1);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingNextStepLatin);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin2);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin3);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin4);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatinCyrillic);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatinArabic);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatinGreek);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatinHebrew);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin5);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin6);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatinThai);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin7);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin8);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingISOLatin9);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingWindowsLatin2);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingWindowsCyrillic);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingWindowsGreek);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingWindowsLatin5);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingWindowsHebrew);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingWindowsArabic);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingKOI8_R);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingBig5);
|
||||
TRY_ENCODING(metaData, kCFStringEncodingASCII);
|
||||
|
||||
return metaData;
|
||||
}
|
||||
|
||||
#undef TRY_ENCODING
|
||||
|
||||
void HTTP_Stream::readCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo)
|
||||
{
|
||||
HTTP_Stream *THIS = static_cast<HTTP_Stream*>(clientCallBackInfo);
|
||||
|
||||
Stream_Configuration *config = Stream_Configuration::configuration();
|
||||
|
||||
CFStringRef reportedNetworkError = NULL;
|
||||
|
||||
switch (eventType) {
|
||||
case kCFStreamEventHasBytesAvailable: {
|
||||
if (!THIS->m_httpReadBuffer) {
|
||||
THIS->m_httpReadBuffer = new UInt8[config->httpConnectionBufferSize];
|
||||
}
|
||||
|
||||
while (CFReadStreamHasBytesAvailable(stream)) {
|
||||
if (!THIS->m_scheduledInRunLoop) {
|
||||
/*
|
||||
* This is critical - though the stream has data available,
|
||||
* do not try to feed the audio queue with data, if it has
|
||||
* indicated that it doesn't want more data due to buffers
|
||||
* full.
|
||||
*/
|
||||
THIS->m_readPending = true;
|
||||
break;
|
||||
}
|
||||
|
||||
CFIndex bytesRead = CFReadStreamRead(stream, THIS->m_httpReadBuffer, config->httpConnectionBufferSize);
|
||||
|
||||
if (CFReadStreamGetStatus(stream) == kCFStreamStatusError ||
|
||||
bytesRead < 0) {
|
||||
if (THIS->contentLength() > 0) {
|
||||
/*
|
||||
* Try to recover gracefully if we have a non-continuous stream
|
||||
*/
|
||||
Input_Stream_Position currentPosition = THIS->position();
|
||||
|
||||
Input_Stream_Position recoveryPosition;
|
||||
recoveryPosition.start = currentPosition.start + THIS->m_bytesRead;
|
||||
recoveryPosition.end = THIS->contentLength();
|
||||
|
||||
HS_TRACE("Recovering HTTP stream, start %llu\n", recoveryPosition.start);
|
||||
|
||||
THIS->open(recoveryPosition);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
CFErrorRef streamError = CFReadStreamCopyError(stream);
|
||||
|
||||
if (streamError) {
|
||||
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
|
||||
|
||||
if (errorDesc) {
|
||||
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
|
||||
|
||||
CFRelease(errorDesc);
|
||||
}
|
||||
|
||||
CFRelease(streamError);
|
||||
}
|
||||
|
||||
if (THIS->m_delegate) {
|
||||
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
|
||||
|
||||
if (reportedNetworkError) {
|
||||
CFRelease(reportedNetworkError);
|
||||
reportedNetworkError = NULL;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytesRead > 0) {
|
||||
THIS->m_bytesRead += bytesRead;
|
||||
|
||||
HS_TRACE("Read %li bytes, total %llu\n", bytesRead, THIS->m_bytesRead);
|
||||
|
||||
THIS->parseHttpHeadersIfNeeded(THIS->m_httpReadBuffer, bytesRead);
|
||||
|
||||
#ifdef INCLUDE_ID3TAG_SUPPORT
|
||||
if (!THIS->m_icyStream && THIS->m_id3Parser->wantData()) {
|
||||
THIS->m_id3Parser->feedData(THIS->m_httpReadBuffer, (UInt32)bytesRead);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (THIS->m_icyStream) {
|
||||
HS_TRACE("Parsing ICY stream\n");
|
||||
|
||||
THIS->parseICYStream(THIS->m_httpReadBuffer, bytesRead);
|
||||
} else {
|
||||
if (THIS->m_delegate) {
|
||||
HS_TRACE("Not an ICY stream; calling the delegate back\n");
|
||||
|
||||
THIS->m_delegate->streamHasBytesAvailable(THIS->m_httpReadBuffer, (UInt32)bytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reportedNetworkError) {
|
||||
CFRelease(reportedNetworkError);
|
||||
reportedNetworkError = NULL;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case kCFStreamEventEndEncountered: {
|
||||
|
||||
// This should concerns only non-continous streams
|
||||
if (THIS->m_bytesRead < THIS->contentLength()) {
|
||||
HS_TRACE("End of stream, but we have read only %llu bytes on a total of %li. Missing: %llu\n", THIS->m_bytesRead, THIS->contentLength(), (THIS->contentLength() - THIS->m_bytesRead));
|
||||
|
||||
Input_Stream_Position currentPosition = THIS->position();
|
||||
|
||||
Input_Stream_Position recoveryPosition;
|
||||
recoveryPosition.start = currentPosition.start + THIS->m_bytesRead;
|
||||
recoveryPosition.end = THIS->contentLength();
|
||||
|
||||
HS_TRACE("Reopen for the end of the file from byte position: %llu\n", recoveryPosition.start);
|
||||
THIS->close();
|
||||
THIS->open(recoveryPosition);
|
||||
break;
|
||||
}
|
||||
|
||||
if (THIS->m_delegate) {
|
||||
THIS->m_delegate->streamEndEncountered();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kCFStreamEventErrorOccurred: {
|
||||
if (THIS->m_delegate) {
|
||||
CFStringRef reportedNetworkError = NULL;
|
||||
CFErrorRef streamError = CFReadStreamCopyError(stream);
|
||||
|
||||
if (streamError) {
|
||||
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
|
||||
|
||||
if (errorDesc) {
|
||||
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
|
||||
|
||||
CFRelease(errorDesc);
|
||||
}
|
||||
|
||||
CFRelease(streamError);
|
||||
}
|
||||
|
||||
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
|
||||
if (reportedNetworkError) {
|
||||
CFRelease(reportedNetworkError);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace astreamer
|
||||
@ -1,98 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_HTTP_STREAM_H
|
||||
#define ASTREAMER_HTTP_STREAM_H
|
||||
|
||||
#import <CFNetwork/CFNetwork.h>
|
||||
#import <vector>
|
||||
#import <map>
|
||||
#import "input_stream.h"
|
||||
#import "id3_parser.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
class HTTP_Stream : public Input_Stream {
|
||||
private:
|
||||
|
||||
HTTP_Stream(const HTTP_Stream&);
|
||||
HTTP_Stream& operator=(const HTTP_Stream&);
|
||||
|
||||
static CFStringRef httpRequestMethod;
|
||||
static CFStringRef httpUserAgentHeader;
|
||||
static CFStringRef httpRangeHeader;
|
||||
static CFStringRef icyMetaDataHeader;
|
||||
static CFStringRef icyMetaDataValue;
|
||||
|
||||
CFURLRef m_url;
|
||||
CFReadStreamRef m_readStream;
|
||||
bool m_scheduledInRunLoop;
|
||||
bool m_readPending;
|
||||
Input_Stream_Position m_position;
|
||||
|
||||
/* HTTP headers */
|
||||
bool m_httpHeadersParsed;
|
||||
CFStringRef m_contentType;
|
||||
size_t m_contentLength;
|
||||
UInt64 m_bytesRead;
|
||||
|
||||
/* ICY protocol */
|
||||
bool m_icyStream;
|
||||
bool m_icyHeaderCR;
|
||||
bool m_icyHeadersRead;
|
||||
bool m_icyHeadersParsed;
|
||||
|
||||
CFStringRef m_icyName;
|
||||
|
||||
std::vector<CFStringRef> m_icyHeaderLines;
|
||||
size_t m_icyMetaDataInterval;
|
||||
size_t m_dataByteReadCount;
|
||||
size_t m_metaDataBytesRemaining;
|
||||
|
||||
std::vector<UInt8> m_icyMetaData;
|
||||
|
||||
/* Read buffers */
|
||||
UInt8 *m_httpReadBuffer;
|
||||
UInt8 *m_icyReadBuffer;
|
||||
|
||||
ID3_Parser *m_id3Parser;
|
||||
|
||||
CFReadStreamRef createReadStream(CFURLRef url);
|
||||
void parseHttpHeadersIfNeeded(const UInt8 *buf, const CFIndex bufSize);
|
||||
void parseICYStream(const UInt8 *buf, const CFIndex bufSize);
|
||||
CFStringRef createMetaDataStringWithMostReasonableEncoding(const UInt8 *bytes, const CFIndex numBytes);
|
||||
|
||||
static void readCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo);
|
||||
|
||||
public:
|
||||
HTTP_Stream();
|
||||
virtual ~HTTP_Stream();
|
||||
|
||||
Input_Stream_Position position();
|
||||
|
||||
CFStringRef contentType();
|
||||
size_t contentLength();
|
||||
|
||||
bool open();
|
||||
bool open(const Input_Stream_Position& position);
|
||||
void close();
|
||||
|
||||
void setScheduledInRunLoop(bool scheduledInRunLoop);
|
||||
|
||||
void setUrl(CFURLRef url);
|
||||
|
||||
static bool canHandleUrl(CFURLRef url);
|
||||
|
||||
/* ID3_Parser_Delegate */
|
||||
void id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
|
||||
void id3tagSizeAvailable(UInt32 tagSize);
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_HTTP_STREAM_H
|
||||
@ -1,536 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "id3_parser.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
//#define ID3_DEBUG 1
|
||||
|
||||
#if !defined ( ID3_DEBUG)
|
||||
#define ID3_TRACE(...) do {} while (0)
|
||||
#else
|
||||
#define ID3_TRACE(...) printf(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
// Code from:
|
||||
// http://www.opensource.apple.com/source/libsecurity_manifest/libsecurity_manifest-29384/lib/SecureDownloadInternal.c
|
||||
|
||||
// Returns a CFString containing the base64 representation of the data.
|
||||
// boolean argument for whether to line wrap at 64 columns or not.
|
||||
CFStringRef createBase64EncodedString(const UInt8* ptr, size_t len, int wrap) {
|
||||
const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/=";
|
||||
|
||||
// base64 encoded data uses 4 ASCII characters to represent 3 octets.
|
||||
// There can be up to two == at the end of the base64 data for padding.
|
||||
// If we are line wrapping then we need space for one newline character
|
||||
// every 64 characters of output.
|
||||
// Rounded 4/3 up to 2 to avoid floating point math.
|
||||
|
||||
//CFIndex max_len = (2*len) + 2;
|
||||
//if (wrap) len = len + ((2*len) / 64) + 1;
|
||||
|
||||
CFMutableStringRef string = CFStringCreateMutable(NULL, 0);
|
||||
if (!string) return NULL;
|
||||
|
||||
/*
|
||||
http://www.faqs.org/rfcs/rfc3548.html
|
||||
+--first octet--+-second octet--+--third octet--+
|
||||
|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|
|
||||
+-----------+---+-------+-------+---+-----------+
|
||||
|5 4 3 2 1 0|5 4 3 2 1 0|5 4 3 2 1 0|5 4 3 2 1 0|
|
||||
+--1.index--+--2.index--+--3.index--+--4.index--+
|
||||
*/
|
||||
int i = 0; // octet offset into input data
|
||||
int column = 0; // output column number (used for line wrapping)
|
||||
for (;;) {
|
||||
UniChar c[16]; // buffer of characters to add to output
|
||||
int j = 0; // offset to place next character in buffer
|
||||
int index; // index into output alphabet
|
||||
|
||||
#define ADDCHAR(_X_) do { c[j++] = _X_; if (wrap && (++column == 64)) { column = 0; c[j++] = '\n'; } } while (0);
|
||||
|
||||
// 1.index
|
||||
index = (ptr[i] >> 2) & 0x3F;
|
||||
ADDCHAR(alphabet[index]);
|
||||
|
||||
// 2.index
|
||||
index = (ptr[i] << 4) & 0x30;
|
||||
if ((i+1) < len) {
|
||||
index = index | ((ptr[i+1] >> 4) & 0x0F);
|
||||
ADDCHAR(alphabet[index]);
|
||||
} else { // end of input, pad as necessary
|
||||
ADDCHAR(alphabet[index]);
|
||||
ADDCHAR('=');
|
||||
ADDCHAR('=');
|
||||
}
|
||||
|
||||
// 3.index
|
||||
if ((i+1) < len) {
|
||||
index = (ptr[i+1] << 2) & 0x3C;
|
||||
if ((i+2) < len) {
|
||||
index = index | ((ptr[i+2] >> 6) & 0x03);
|
||||
ADDCHAR(alphabet[index]);
|
||||
} else { // end of input, pad as necessary
|
||||
ADDCHAR(alphabet[index]);
|
||||
ADDCHAR('=');
|
||||
}
|
||||
}
|
||||
|
||||
// 4.index
|
||||
if ((i+2) < len) {
|
||||
index = (ptr[i+2]) & 0x3F;
|
||||
ADDCHAR(alphabet[index]);
|
||||
}
|
||||
|
||||
CFStringAppendCharacters(string, c, j);
|
||||
i += 3; // we processed 3 bytes of input
|
||||
if (i >= len) {
|
||||
// end of data, append newline if we haven't already
|
||||
if (wrap && c[j-1] != '\n') {
|
||||
c[0] = '\n';
|
||||
CFStringAppendCharacters(string, c, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
enum ID3_Parser_State {
|
||||
ID3_Parser_State_Initial = 0,
|
||||
ID3_Parser_State_Parse_Frames,
|
||||
ID3_Parser_State_Tag_Parsed,
|
||||
ID3_Parser_State_Not_Valid_Tag
|
||||
};
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Private class
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
class ID3_Parser_Private {
|
||||
public:
|
||||
ID3_Parser_Private();
|
||||
~ID3_Parser_Private();
|
||||
|
||||
bool wantData();
|
||||
void feedData(UInt8 *data, UInt32 numBytes);
|
||||
void setState(ID3_Parser_State state);
|
||||
void reset();
|
||||
|
||||
CFStringRef parseContent(UInt32 framesize, UInt32 pos, CFStringEncoding encoding, bool byteOrderMark);
|
||||
|
||||
ID3_Parser *m_parser;
|
||||
ID3_Parser_State m_state;
|
||||
UInt32 m_bytesReceived;
|
||||
UInt32 m_tagSize;
|
||||
UInt8 m_majorVersion;
|
||||
bool m_hasFooter;
|
||||
bool m_usesUnsynchronisation;
|
||||
bool m_usesExtendedHeader;
|
||||
CFStringRef m_title;
|
||||
CFStringRef m_performer;
|
||||
CFStringRef m_coverArt;
|
||||
|
||||
std::vector<UInt8> m_tagData;
|
||||
};
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* Private class implementation
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
ID3_Parser_Private::ID3_Parser_Private() :
|
||||
m_parser(0),
|
||||
m_state(ID3_Parser_State_Initial),
|
||||
m_bytesReceived(0),
|
||||
m_tagSize(0),
|
||||
m_majorVersion(0),
|
||||
m_hasFooter(false),
|
||||
m_usesUnsynchronisation(false),
|
||||
m_usesExtendedHeader(false),
|
||||
m_title(NULL),
|
||||
m_performer(NULL),
|
||||
m_coverArt(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
ID3_Parser_Private::~ID3_Parser_Private()
|
||||
{
|
||||
if (m_performer) {
|
||||
CFRelease(m_performer);
|
||||
m_performer = NULL;
|
||||
}
|
||||
if (m_title) {
|
||||
CFRelease(m_title);
|
||||
m_title = NULL;
|
||||
}
|
||||
if (m_coverArt) {
|
||||
CFRelease(m_coverArt);
|
||||
m_coverArt = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bool ID3_Parser_Private::wantData()
|
||||
{
|
||||
if (m_state == ID3_Parser_State_Tag_Parsed) {
|
||||
return false;
|
||||
}
|
||||
if (m_state == ID3_Parser_State_Not_Valid_Tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ID3_Parser_Private::feedData(UInt8 *data, UInt32 numBytes)
|
||||
{
|
||||
if (!wantData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_bytesReceived += numBytes;
|
||||
|
||||
ID3_TRACE("received %i bytes, total bytes %i\n", numBytes, m_bytesReceived);
|
||||
|
||||
for (CFIndex i=0; i < numBytes; i++) {
|
||||
m_tagData.push_back(data[i]);
|
||||
}
|
||||
|
||||
bool enoughBytesToParse = true;
|
||||
|
||||
while (enoughBytesToParse) {
|
||||
switch (m_state) {
|
||||
case ID3_Parser_State_Initial: {
|
||||
// Do we have enough bytes to determine if this is an ID3 tag or not?
|
||||
if (m_bytesReceived <= 9) {
|
||||
enoughBytesToParse = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!(m_tagData[0] == 'I' &&
|
||||
m_tagData[1] == 'D' &&
|
||||
m_tagData[2] == '3')) {
|
||||
ID3_TRACE("Not an ID3 tag, bailing out\n");
|
||||
|
||||
// Does not begin with the tag header; not an ID3 tag
|
||||
setState(ID3_Parser_State_Not_Valid_Tag);
|
||||
enoughBytesToParse = false;
|
||||
break;
|
||||
}
|
||||
|
||||
m_majorVersion = m_tagData[3];
|
||||
// Currently support only id3v2.2 and 2.3
|
||||
if (m_majorVersion != 2 && m_majorVersion != 3) {
|
||||
ID3_TRACE("ID3v2.%i not supported by the parser\n", m_majorVersion);
|
||||
|
||||
setState(ID3_Parser_State_Not_Valid_Tag);
|
||||
enoughBytesToParse = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Ignore the revision
|
||||
|
||||
// Parse the flags
|
||||
|
||||
if ((m_tagData[5] & 0x80) != 0) {
|
||||
m_usesUnsynchronisation = true;
|
||||
} else if ((m_tagData[5] & 0x40) != 0 && m_majorVersion >= 3) {
|
||||
m_usesExtendedHeader = true;
|
||||
} else if ((m_tagData[5] & 0x10) != 0 && m_majorVersion >= 3) {
|
||||
m_hasFooter = true;
|
||||
}
|
||||
|
||||
m_tagSize = ((m_tagData[6] & 0x7F) << 21) | ((m_tagData[7] & 0x7F) << 14) |
|
||||
((m_tagData[8] & 0x7F) << 7) | (m_tagData[9] & 0x7F);
|
||||
|
||||
if (m_tagSize > 0) {
|
||||
if (m_hasFooter) {
|
||||
m_tagSize += 10;
|
||||
}
|
||||
m_tagSize += 10;
|
||||
|
||||
ID3_TRACE("tag size: %i\n", m_tagSize);
|
||||
|
||||
if (m_parser->m_delegate) {
|
||||
m_parser->m_delegate->id3tagSizeAvailable(m_tagSize);
|
||||
}
|
||||
|
||||
setState(ID3_Parser_State_Parse_Frames);
|
||||
break;
|
||||
}
|
||||
|
||||
setState(ID3_Parser_State_Not_Valid_Tag);
|
||||
enoughBytesToParse = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case ID3_Parser_State_Parse_Frames: {
|
||||
// Do we have enough data to parse the frames?
|
||||
if (m_tagData.size() < m_tagSize) {
|
||||
ID3_TRACE("Not enough data received for parsing, have %lu bytes, need %i bytes\n",
|
||||
m_tagData.size(),
|
||||
m_tagSize);
|
||||
enoughBytesToParse = false;
|
||||
break;
|
||||
}
|
||||
|
||||
UInt32 pos = 10;
|
||||
|
||||
// Do we have an extended header? If we do, skip it
|
||||
if (m_usesExtendedHeader) {
|
||||
UInt32 extendedHeaderSize = ((m_tagData[pos] << 21) |
|
||||
(m_tagData[pos+1] << 14) |
|
||||
(m_tagData[pos+2] << 7) |
|
||||
m_tagData[pos+3]);
|
||||
|
||||
if (pos + extendedHeaderSize >= m_tagSize) {
|
||||
setState(ID3_Parser_State_Not_Valid_Tag);
|
||||
enoughBytesToParse = false;
|
||||
break;
|
||||
}
|
||||
|
||||
ID3_TRACE("Skipping extended header, size %i\n", extendedHeaderSize);
|
||||
|
||||
pos += extendedHeaderSize;
|
||||
}
|
||||
|
||||
while (pos < m_tagSize) {
|
||||
char frameName[5];
|
||||
frameName[0] = m_tagData[pos];
|
||||
frameName[1] = m_tagData[pos+1];
|
||||
frameName[2] = m_tagData[pos+2];
|
||||
|
||||
if (m_majorVersion >= 3) {
|
||||
frameName[3] = m_tagData[pos+3];
|
||||
} else {
|
||||
frameName[3] = 0;
|
||||
}
|
||||
frameName[4] = 0;
|
||||
|
||||
UInt32 framesize = 0;
|
||||
|
||||
if (m_majorVersion >= 3) {
|
||||
pos += 4;
|
||||
|
||||
framesize = ((m_tagData[pos] << 21) |
|
||||
(m_tagData[pos+1] << 14) |
|
||||
(m_tagData[pos+2] << 7) |
|
||||
m_tagData[pos+3]);
|
||||
} else {
|
||||
pos += 3;
|
||||
|
||||
framesize = ((m_tagData[pos] << 16) |
|
||||
(m_tagData[pos+1] << 8) |
|
||||
m_tagData[pos+2]);
|
||||
}
|
||||
|
||||
if (framesize == 0) {
|
||||
setState(ID3_Parser_State_Not_Valid_Tag);
|
||||
enoughBytesToParse = false;
|
||||
// Break from the loop and then out of the case context
|
||||
goto ParseFramesExit;
|
||||
}
|
||||
|
||||
if (m_majorVersion >= 3) {
|
||||
pos += 6;
|
||||
} else {
|
||||
pos += 3;
|
||||
}
|
||||
|
||||
CFStringEncoding encoding;
|
||||
bool byteOrderMark = false;
|
||||
|
||||
if (m_tagData[pos] == 3) {
|
||||
encoding = kCFStringEncodingUTF8;
|
||||
} else if (m_tagData[pos] == 2) {
|
||||
encoding = kCFStringEncodingUTF16BE;
|
||||
} else if (m_tagData[pos] == 1) {
|
||||
encoding = kCFStringEncodingUTF16;
|
||||
byteOrderMark = true;
|
||||
} else {
|
||||
// ISO-8859-1 is the default encoding
|
||||
encoding = kCFStringEncodingISOLatin1;
|
||||
}
|
||||
|
||||
if (!strcmp(frameName, "TIT2") || !strcmp(frameName, "TT2")) {
|
||||
if (m_title) {
|
||||
CFRelease(m_title);
|
||||
}
|
||||
m_title = parseContent(framesize, pos + 1, encoding, byteOrderMark);
|
||||
|
||||
ID3_TRACE("ID3 title parsed: '%s'\n", CFStringGetCStringPtr(m_title, CFStringGetSystemEncoding()));
|
||||
} else if (!strcmp(frameName, "TPE1") || !strcmp(frameName, "TP1")) {
|
||||
if (m_performer) {
|
||||
CFRelease(m_performer);
|
||||
}
|
||||
m_performer = parseContent(framesize, pos + 1, encoding, byteOrderMark);
|
||||
|
||||
ID3_TRACE("ID3 performer parsed: '%s'\n", CFStringGetCStringPtr(m_performer, CFStringGetSystemEncoding()));
|
||||
} else if (!strcmp(frameName, "APIC")) {
|
||||
char imageType[65] = {0};
|
||||
|
||||
size_t dataPos = pos+1;
|
||||
|
||||
for (int i=0; m_tagData[dataPos]; i++,dataPos++) {
|
||||
imageType[i] = m_tagData[dataPos];
|
||||
}
|
||||
dataPos++;
|
||||
|
||||
if (!strcmp(imageType, "image/jpeg") ||
|
||||
!strcmp(imageType, "image/png")) {
|
||||
|
||||
ID3_TRACE("Image type %s, parsing, dataPos %zu\n", imageType, dataPos);
|
||||
|
||||
// Skip the image description
|
||||
while (!m_tagData[++dataPos]);
|
||||
|
||||
const size_t coverArtSize = framesize - ((dataPos - pos) + 5);
|
||||
|
||||
UInt8 *bytes = new UInt8[coverArtSize];
|
||||
|
||||
for (int i=0; i < coverArtSize; i++) {
|
||||
bytes[i] = m_tagData[dataPos+i];
|
||||
}
|
||||
|
||||
if (m_coverArt) {
|
||||
CFRelease(m_coverArt);
|
||||
}
|
||||
m_coverArt = createBase64EncodedString(bytes, coverArtSize, 0);
|
||||
|
||||
delete [] bytes;
|
||||
} else {
|
||||
ID3_TRACE("%s is an unknown type for image data, skipping\n", imageType);
|
||||
}
|
||||
} else {
|
||||
// Unknown/unhandled frame
|
||||
ID3_TRACE("Unknown/unhandled frame: %s, size %i\n", frameName, framesize);
|
||||
}
|
||||
|
||||
pos += framesize;
|
||||
}
|
||||
|
||||
// Push out the metadata
|
||||
if (m_parser->m_delegate) {
|
||||
std::map<CFStringRef,CFStringRef> metadataMap;
|
||||
|
||||
if (m_performer && CFStringGetLength(m_performer) > 0) {
|
||||
metadataMap[CFSTR("MPMediaItemPropertyArtist")] =
|
||||
CFStringCreateCopy(kCFAllocatorDefault, m_performer);
|
||||
}
|
||||
|
||||
if (m_title && CFStringGetLength(m_title) > 0) {
|
||||
metadataMap[CFSTR("MPMediaItemPropertyTitle")] =
|
||||
CFStringCreateCopy(kCFAllocatorDefault, m_title);
|
||||
}
|
||||
|
||||
if (m_coverArt && CFStringGetLength(m_coverArt) > 0) {
|
||||
metadataMap[CFSTR("CoverArt")] =
|
||||
CFStringCreateCopy(kCFAllocatorDefault, m_coverArt);
|
||||
}
|
||||
|
||||
m_parser->m_delegate->id3metaDataAvailable(metadataMap);
|
||||
}
|
||||
|
||||
setState(ID3_Parser_State_Tag_Parsed);
|
||||
enoughBytesToParse = false;
|
||||
ParseFramesExit:
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
enoughBytesToParse = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ID3_Parser_Private::setState(astreamer::ID3_Parser_State state)
|
||||
{
|
||||
m_state = state;
|
||||
}
|
||||
|
||||
void ID3_Parser_Private::reset()
|
||||
{
|
||||
m_state = ID3_Parser_State_Initial;
|
||||
m_bytesReceived = 0;
|
||||
m_tagSize = 0;
|
||||
m_majorVersion = 0;
|
||||
m_hasFooter = false;
|
||||
m_usesUnsynchronisation = false;
|
||||
m_usesExtendedHeader = false;
|
||||
|
||||
if (m_title) {
|
||||
CFRelease(m_title);
|
||||
m_title = NULL;
|
||||
}
|
||||
if (m_performer) {
|
||||
CFRelease(m_performer);
|
||||
m_performer = NULL;
|
||||
}
|
||||
if (m_coverArt) {
|
||||
CFRelease(m_coverArt);
|
||||
m_coverArt = NULL;
|
||||
}
|
||||
|
||||
m_tagData.clear();
|
||||
}
|
||||
|
||||
CFStringRef ID3_Parser_Private::parseContent(UInt32 framesize, UInt32 pos, CFStringEncoding encoding, bool byteOrderMark)
|
||||
{
|
||||
CFStringRef content = CFStringCreateWithBytes(kCFAllocatorDefault,
|
||||
&m_tagData[pos],
|
||||
framesize - 1,
|
||||
encoding,
|
||||
byteOrderMark);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/*
|
||||
* =======================================
|
||||
* ID3_Parser implementation
|
||||
* =======================================
|
||||
*/
|
||||
|
||||
ID3_Parser::ID3_Parser() :
|
||||
m_delegate(0),
|
||||
m_private(new ID3_Parser_Private())
|
||||
{
|
||||
m_private->m_parser = this;
|
||||
}
|
||||
|
||||
ID3_Parser::~ID3_Parser()
|
||||
{
|
||||
delete m_private;
|
||||
m_private = 0;
|
||||
}
|
||||
|
||||
void ID3_Parser::reset()
|
||||
{
|
||||
m_private->reset();
|
||||
}
|
||||
|
||||
bool ID3_Parser::wantData()
|
||||
{
|
||||
return m_private->wantData();
|
||||
}
|
||||
|
||||
void ID3_Parser::feedData(UInt8 *data, UInt32 numBytes)
|
||||
{
|
||||
m_private->feedData(data, numBytes);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_ID3_PARSER_H
|
||||
#define ASTREAMER_ID3_PARSER_H
|
||||
|
||||
#include <map>
|
||||
|
||||
#import <CFNetwork/CFNetwork.h>
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
class ID3_Parser_Delegate;
|
||||
class ID3_Parser_Private;
|
||||
|
||||
class ID3_Parser {
|
||||
public:
|
||||
ID3_Parser();
|
||||
~ID3_Parser();
|
||||
|
||||
void reset();
|
||||
bool wantData();
|
||||
void feedData(UInt8 *data, UInt32 numBytes);
|
||||
|
||||
ID3_Parser_Delegate *m_delegate;
|
||||
|
||||
private:
|
||||
ID3_Parser_Private *m_private;
|
||||
};
|
||||
|
||||
class ID3_Parser_Delegate {
|
||||
public:
|
||||
virtual void id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData) = 0;
|
||||
virtual void id3tagSizeAvailable(UInt32 tagSize) = 0;
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_ID3_PARSER_H
|
||||
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "input_stream.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
Input_Stream::Input_Stream() : m_delegate(0)
|
||||
{
|
||||
}
|
||||
|
||||
Input_Stream::~Input_Stream()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_INPUT_STREAM_H
|
||||
#define ASTREAMER_INPUT_STREAM_H
|
||||
|
||||
#import "id3_parser.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
class Input_Stream_Delegate;
|
||||
|
||||
struct Input_Stream_Position {
|
||||
UInt64 start;
|
||||
UInt64 end;
|
||||
};
|
||||
|
||||
class Input_Stream : public ID3_Parser_Delegate {
|
||||
public:
|
||||
Input_Stream();
|
||||
virtual ~Input_Stream();
|
||||
|
||||
Input_Stream_Delegate* m_delegate;
|
||||
|
||||
virtual Input_Stream_Position position() = 0;
|
||||
|
||||
virtual CFStringRef contentType() = 0;
|
||||
virtual size_t contentLength() = 0;
|
||||
|
||||
virtual bool open() = 0;
|
||||
virtual bool open(const Input_Stream_Position& position) = 0;
|
||||
virtual void close() = 0;
|
||||
|
||||
virtual void setScheduledInRunLoop(bool scheduledInRunLoop) = 0;
|
||||
|
||||
virtual void setUrl(CFURLRef url) = 0;
|
||||
};
|
||||
|
||||
class Input_Stream_Delegate {
|
||||
public:
|
||||
virtual void streamIsReadyRead() = 0;
|
||||
virtual void streamHasBytesAvailable(UInt8 *data, UInt32 numBytes) = 0;
|
||||
virtual void streamEndEncountered() = 0;
|
||||
virtual void streamErrorOccurred(CFStringRef errorDesc) = 0;
|
||||
virtual void streamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData) = 0;
|
||||
virtual void streamMetaDataByteSizeAvailable(UInt32 sizeInBytes) = 0;
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_INPUT_STREAM_H
|
||||
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "stream_configuration.h"
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
Stream_Configuration::Stream_Configuration() :
|
||||
userAgent(NULL),
|
||||
cacheDirectory(NULL),
|
||||
predefinedHttpHeaderValues(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
Stream_Configuration::~Stream_Configuration()
|
||||
{
|
||||
if (userAgent) {
|
||||
CFRelease(userAgent);
|
||||
userAgent = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
Stream_Configuration* Stream_Configuration::configuration()
|
||||
{
|
||||
static Stream_Configuration config;
|
||||
return &config;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef ASTREAMER_STREAM_CONFIGURATION_H
|
||||
#define ASTREAMER_STREAM_CONFIGURATION_H
|
||||
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
namespace astreamer {
|
||||
|
||||
struct Stream_Configuration {
|
||||
unsigned bufferCount;
|
||||
unsigned bufferSize;
|
||||
unsigned maxPacketDescs;
|
||||
unsigned httpConnectionBufferSize;
|
||||
double outputSampleRate;
|
||||
long outputNumChannels;
|
||||
int bounceInterval;
|
||||
int maxBounceCount;
|
||||
int startupWatchdogPeriod;
|
||||
int maxPrebufferedByteCount;
|
||||
bool usePrebufferSizeCalculationInSeconds;
|
||||
bool usePrebufferSizeCalculationInPackets;
|
||||
int requiredInitialPrebufferedByteCountForContinuousStream;
|
||||
int requiredInitialPrebufferedByteCountForNonContinuousStream;
|
||||
int requiredPrebufferSizeInSeconds;
|
||||
int requiredInitialPrebufferedPacketCount;
|
||||
CFStringRef userAgent;
|
||||
CFStringRef cacheDirectory;
|
||||
CFDictionaryRef predefinedHttpHeaderValues;
|
||||
bool cacheEnabled;
|
||||
bool seekingFromCacheEnabled;
|
||||
bool automaticAudioSessionHandlingEnabled;
|
||||
bool enableTimeAndPitchConversion;
|
||||
bool requireStrictContentTypeChecking;
|
||||
int maxDiskCacheSize;
|
||||
|
||||
static Stream_Configuration *configuration();
|
||||
|
||||
private:
|
||||
Stream_Configuration();
|
||||
~Stream_Configuration();
|
||||
|
||||
Stream_Configuration(const Stream_Configuration&);
|
||||
Stream_Configuration& operator=(const Stream_Configuration&);
|
||||
};
|
||||
|
||||
} // namespace astreamer
|
||||
|
||||
#endif // ASTREAMER_STREAM_CONFIGURATION_H
|
||||
53
Pods/FreeStreamer/LICENSE.txt
generated
53
Pods/FreeStreamer/LICENSE.txt
generated
@ -1,53 +0,0 @@
|
||||
Copyright (c) 2011-2018 Matias Muhonen <mmu@iki.fi> 穆马帝
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
The FreeStreamer framework bundles Reachability which is licensed under the following
|
||||
license:
|
||||
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
41
Pods/FreeStreamer/README.markdown
generated
41
Pods/FreeStreamer/README.markdown
generated
@ -1,41 +0,0 @@
|
||||
FreeStreamer
|
||||
====================
|
||||
|
||||
A streaming audio player for iOS and OS X.
|
||||
|
||||
Features
|
||||
====================
|
||||
|
||||
- **CPU-friendly** design (uses 1% of CPU on average when streaming)
|
||||
- **Multiple protocols supported**: ShoutCast, standard HTTP, local files
|
||||
- **Prepared for tough network conditions**: adjustable buffer sizes, stream pre-buffering and restart on failures
|
||||
- **Metadata support**: ShoutCast metadata, IDv2 tags
|
||||
- **Local disk caching**: user only needs to stream a file once and after that it can be played from a local cache
|
||||
- **Preloading**: playback can start immediately without needing to wait for buffering
|
||||
- **Record**: support recording the stream contents to a file
|
||||
- **Access the PCM audio samples**: as an example, a visualizer is included
|
||||
|
||||
Documentation
|
||||
====================
|
||||
|
||||
See the [FAQ](https://github.com/muhku/FreeStreamer/wiki/FreeStreamer-FAQ) (Frequently Asked Questions) in the wiki. We also have an [API documentation](http://muhku.github.io/api/) available. The [usage instructions](https://github.com/muhku/FreeStreamer/wiki/Using-the-player-in-your-own-project) are also covered in the wiki.
|
||||
|
||||
Is somebody using this in real life?
|
||||
====================
|
||||
|
||||
The short answer is yes! Check out our [website](http://muhku.github.io/) for the reference applications.
|
||||
|
||||
Reporting bugs and contributing
|
||||
====================
|
||||
|
||||
For code contributions and other questions, it is preferrable to create a Github pull request. I don't have time for private email support, so usually the best way to get help is to interact with Github issues.
|
||||
|
||||
License
|
||||
====================
|
||||
|
||||
See [LICENSE.txt](https://github.com/muhku/FreeStreamer/blob/master/LICENSE.txt) for the license.
|
||||
|
||||
Donations
|
||||
====================
|
||||
|
||||
It is possible to use [PayPal](http://muhku.github.io/donate.html) for donations.
|
||||
10
Pods/Manifest.lock
generated
10
Pods/Manifest.lock
generated
@ -1,14 +1,11 @@
|
||||
PODS:
|
||||
- Alamofire (5.9.1)
|
||||
- DownloadButton (0.1.0)
|
||||
- FreeStreamer (4.0.0):
|
||||
- Reachability (~> 3.0)
|
||||
- IQKeyboardManagerSwift (6.5.16)
|
||||
- JXPagingView/Paging (2.1.3)
|
||||
- JXSegmentedView (1.3.3)
|
||||
- Kingfisher (7.11.0)
|
||||
- MJRefresh (3.7.9)
|
||||
- Reachability (3.7.6)
|
||||
- SnapKit (5.7.1)
|
||||
- SVProgressHUD (2.3.1):
|
||||
- SVProgressHUD/Core (= 2.3.1)
|
||||
@ -19,7 +16,6 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- DownloadButton
|
||||
- FreeStreamer
|
||||
- IQKeyboardManagerSwift
|
||||
- JXPagingView/Paging
|
||||
- JXSegmentedView
|
||||
@ -34,13 +30,11 @@ SPEC REPOS:
|
||||
trunk:
|
||||
- Alamofire
|
||||
- DownloadButton
|
||||
- FreeStreamer
|
||||
- IQKeyboardManagerSwift
|
||||
- JXPagingView
|
||||
- JXSegmentedView
|
||||
- Kingfisher
|
||||
- MJRefresh
|
||||
- Reachability
|
||||
- SnapKit
|
||||
- SVProgressHUD
|
||||
- SwiftDate
|
||||
@ -49,18 +43,16 @@ SPEC REPOS:
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
|
||||
DownloadButton: 49a21a89e0d7d1b42d9134f79aaa40e727cd57c3
|
||||
FreeStreamer: 7e9c976045701ac2f7e9c14c17245203c37bf2ea
|
||||
IQKeyboardManagerSwift: 12d89768845bb77b55cc092ecc2b1f9370f06b76
|
||||
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
|
||||
JXSegmentedView: 651b60fcf705258ba9395edd53876dbd2853fb68
|
||||
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
|
||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||
Reachability: fd0ecd23705e2599e4cceeb943222ae02296cbc6
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22
|
||||
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
|
||||
Tiercel: c0a73f876a72800333b15f4e7e48791f4ad21e90
|
||||
|
||||
PODFILE CHECKSUM: 2aaf13c9ed7d2b78bcf21030bbbacf03b3a0212c
|
||||
PODFILE CHECKSUM: 436104abc66aacc2c16f90adefe5230845e81453
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
5261
Pods/Pods.xcodeproj/project.pbxproj
generated
5261
Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C3AAC0817EA4DC8BD9C0046F50078BF9"
|
||||
BuildableName = "FreeStreamer.framework"
|
||||
BlueprintName = "FreeStreamer"
|
||||
ReferencedContainer = "container:Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D2787856C227A709315E3C9C4355A440"
|
||||
BuildableName = "Reachability_Privacy.bundle"
|
||||
BlueprintName = "Reachability-Reachability_Privacy"
|
||||
ReferencedContainer = "container:Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CAA047C0F5E4106F3904E8497FA17F97"
|
||||
BuildableName = "Reachability.framework"
|
||||
BlueprintName = "Reachability"
|
||||
ReferencedContainer = "container:Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -8,155 +8,96 @@
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>Alamofire.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>DownloadButton-DownloadButton.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>DownloadButton.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>FreeStreamer.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>IQKeyboardManagerSwift.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
<key>JXPagingView-JXPagingView.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
<key>JXPagingView.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>6</integer>
|
||||
</dict>
|
||||
<key>JXSegmentedView-JXSegmentedView.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>9</integer>
|
||||
</dict>
|
||||
<key>JXSegmentedView.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>8</integer>
|
||||
</dict>
|
||||
<key>Kingfisher-Kingfisher.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>11</integer>
|
||||
</dict>
|
||||
<key>Kingfisher.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>10</integer>
|
||||
</dict>
|
||||
<key>MJRefresh-MJRefresh.Privacy.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>13</integer>
|
||||
</dict>
|
||||
<key>MJRefresh.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>12</integer>
|
||||
</dict>
|
||||
<key>Pods-relax.offline.mp3.music.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
<key>Reachability-Reachability_Privacy.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>16</integer>
|
||||
</dict>
|
||||
<key>Reachability.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
<key>SVProgressHUD.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>19</integer>
|
||||
</dict>
|
||||
<key>SnapKit-SnapKit_Privacy.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>18</integer>
|
||||
</dict>
|
||||
<key>SnapKit.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>17</integer>
|
||||
</dict>
|
||||
<key>SwiftDate.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>Tiercel.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
||||
14
Pods/Reachability/Framework/PrivacyInfo.xcprivacy
generated
14
Pods/Reachability/Framework/PrivacyInfo.xcprivacy
generated
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
24
Pods/Reachability/LICENCE.txt
generated
24
Pods/Reachability/LICENCE.txt
generated
@ -1,24 +0,0 @@
|
||||
Copyright (c) 2011-2013, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
159
Pods/Reachability/README.md
generated
159
Pods/Reachability/README.md
generated
@ -1,159 +0,0 @@
|
||||
[](https://www.versioneye.com/objective-c/reachability/references)
|
||||
[](https://github.com/tonymillion/Reachability/actions)
|
||||
|
||||
# **WARNING** there have been reports of apps being rejected when Reachability is used in a framework. The only solution to this so far is to rename the class.
|
||||
|
||||
# Reachability
|
||||
|
||||
This is a drop-in replacement for Apple's `Reachability` class. It is ARC-compatible, and it uses the new GCD methods to notify of network interface changes.
|
||||
|
||||
In addition to the standard `NSNotification`, it supports the use of blocks for when the network becomes reachable and unreachable.
|
||||
|
||||
Finally, you can specify whether a WWAN connection is considered "reachable".
|
||||
|
||||
*DO NOT OPEN BUGS UNTIL YOU HAVE TESTED ON DEVICE*
|
||||
|
||||
**BEFORE YOU OPEN A BUG ABOUT iOS6/iOS5 build errors, use Tag 3.2 or 3.1 as they support assign types**
|
||||
|
||||
## Requirements
|
||||
|
||||
Once you have added the `.h/m` files to your project, simply:
|
||||
|
||||
* Go to the `Project->TARGETS->Build Phases->Link Binary With Libraries`.
|
||||
* Press the plus in the lower left of the list.
|
||||
* Add `SystemConfiguration.framework`.
|
||||
|
||||
Boom, you're done.
|
||||
|
||||
## Examples
|
||||
|
||||
### Block Example
|
||||
|
||||
This sample uses blocks to notify when the interface state has changed. The blocks will be called on a **BACKGROUND THREAD**, so you need to dispatch UI updates onto the main thread.
|
||||
|
||||
#### In Objective-C
|
||||
|
||||
```objc
|
||||
// Allocate a reachability object
|
||||
Reachability* reach = [Reachability reachabilityWithHostname:@"www.google.com"];
|
||||
|
||||
// Set the blocks
|
||||
reach.reachableBlock = ^(Reachability*reach)
|
||||
{
|
||||
// keep in mind this is called on a background thread
|
||||
// and if you are updating the UI it needs to happen
|
||||
// on the main thread, like this:
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSLog(@"REACHABLE!");
|
||||
});
|
||||
};
|
||||
|
||||
reach.unreachableBlock = ^(Reachability*reach)
|
||||
{
|
||||
NSLog(@"UNREACHABLE!");
|
||||
};
|
||||
|
||||
// Start the notifier, which will cause the reachability object to retain itself!
|
||||
[reach startNotifier];
|
||||
```
|
||||
|
||||
### In Swift 3
|
||||
|
||||
```swift
|
||||
import Reachability
|
||||
|
||||
var reach: Reachability?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
// Allocate a reachability object
|
||||
self.reach = Reachability.forInternetConnection()
|
||||
|
||||
// Set the blocks
|
||||
self.reach!.reachableBlock = {
|
||||
(reach: Reachability?) -> Void in
|
||||
|
||||
// keep in mind this is called on a background thread
|
||||
// and if you are updating the UI it needs to happen
|
||||
// on the main thread, like this:
|
||||
DispatchQueue.main.async {
|
||||
print("REACHABLE!")
|
||||
}
|
||||
}
|
||||
|
||||
self.reach!.unreachableBlock = {
|
||||
(reach: Reachability?) -> Void in
|
||||
print("UNREACHABLE!")
|
||||
}
|
||||
|
||||
self.reach!.startNotifier()
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
### `NSNotification` Example
|
||||
|
||||
This sample will use `NSNotification`s to notify when the interface has changed. They will be delivered on the **MAIN THREAD**, so you *can* do UI updates from within the function.
|
||||
|
||||
In addition, it asks the `Reachability` object to consider the WWAN (3G/EDGE/CDMA) as a non-reachable connection (you might use this if you are writing a video streaming app, for example, to save the user's data plan).
|
||||
|
||||
#### In Objective-C
|
||||
|
||||
```objc
|
||||
// Allocate a reachability object
|
||||
Reachability* reach = [Reachability reachabilityWithHostname:@"www.google.com"];
|
||||
|
||||
// Tell the reachability that we DON'T want to be reachable on 3G/EDGE/CDMA
|
||||
reach.reachableOnWWAN = NO;
|
||||
|
||||
// Here we set up a NSNotification observer. The Reachability that caused the notification
|
||||
// is passed in the object parameter
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(reachabilityChanged:)
|
||||
name:kReachabilityChangedNotification
|
||||
object:nil];
|
||||
|
||||
[reach startNotifier];
|
||||
```
|
||||
|
||||
#### In Swift 3
|
||||
|
||||
```swift
|
||||
import Reachability
|
||||
|
||||
var reach: Reachability?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
// Allocate a reachability object
|
||||
self.reach = Reachability.forInternetConnection()
|
||||
|
||||
// Tell the reachability that we DON'T want to be reachable on 3G/EDGE/CDMA
|
||||
self.reach!.reachableOnWWAN = false
|
||||
|
||||
// Here we set up a NSNotification observer. The Reachability that caused the notification
|
||||
// is passed in the object parameter
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(reachabilityChanged),
|
||||
name: NSNotification.Name.reachabilityChanged,
|
||||
object: nil
|
||||
)
|
||||
|
||||
self.reach!.startNotifier()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func reachabilityChanged(notification: NSNotification) {
|
||||
if self.reach!.isReachableViaWiFi() || self.reach!.isReachableViaWWAN() {
|
||||
print("Service available!!!")
|
||||
} else {
|
||||
print("No service available!!!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tell the world
|
||||
|
||||
Head over to [Projects using Reachability](https://github.com/tonymillion/Reachability/wiki/Projects-using-Reachability) and add your project for "Maximum Wins!".
|
||||
103
Pods/Reachability/Reachability.h
generated
103
Pods/Reachability/Reachability.h
generated
@ -1,103 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
//! Project version number for MacOSReachability.
|
||||
FOUNDATION_EXPORT double ReachabilityVersionNumber;
|
||||
|
||||
//! Project version string for MacOSReachability.
|
||||
FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[];
|
||||
|
||||
/**
|
||||
* Create NS_ENUM macro if it does not exist on the targeted version of iOS or OS X.
|
||||
*
|
||||
* @see http://nshipster.com/ns_enum-ns_options/
|
||||
**/
|
||||
#ifndef NS_ENUM
|
||||
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
|
||||
#endif
|
||||
|
||||
extern NSString *const kReachabilityChangedNotification;
|
||||
|
||||
typedef NS_ENUM(NSInteger, NetworkStatus) {
|
||||
// Apple NetworkStatus Compatible Names.
|
||||
NotReachable = 0,
|
||||
ReachableViaWiFi = 2,
|
||||
ReachableViaWWAN = 1
|
||||
};
|
||||
|
||||
@class Reachability;
|
||||
|
||||
typedef void (^NetworkReachable)(Reachability * reachability);
|
||||
typedef void (^NetworkUnreachable)(Reachability * reachability);
|
||||
typedef void (^NetworkReachability)(Reachability * reachability, SCNetworkConnectionFlags flags);
|
||||
|
||||
|
||||
@interface Reachability : NSObject
|
||||
|
||||
@property (nonatomic, copy) NetworkReachable reachableBlock;
|
||||
@property (nonatomic, copy) NetworkUnreachable unreachableBlock;
|
||||
@property (nonatomic, copy) NetworkReachability reachabilityBlock;
|
||||
|
||||
@property (nonatomic, assign) BOOL reachableOnWWAN;
|
||||
|
||||
|
||||
+(instancetype)reachabilityWithHostname:(NSString*)hostname;
|
||||
// This is identical to the function above, but is here to maintain
|
||||
//compatibility with Apples original code. (see .m)
|
||||
+(instancetype)reachabilityWithHostName:(NSString*)hostname;
|
||||
+(instancetype)reachabilityForInternetConnection;
|
||||
+(instancetype)reachabilityWithAddress:(void *)hostAddress;
|
||||
+(instancetype)reachabilityForLocalWiFi;
|
||||
+(instancetype)reachabilityWithURL:(NSURL*)url;
|
||||
|
||||
-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref;
|
||||
|
||||
-(BOOL)startNotifier;
|
||||
-(void)stopNotifier;
|
||||
|
||||
-(BOOL)isReachable;
|
||||
-(BOOL)isReachableViaWWAN;
|
||||
-(BOOL)isReachableViaWiFi;
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
-(BOOL)isConnectionRequired; // Identical DDG variant.
|
||||
-(BOOL)connectionRequired; // Apple's routine.
|
||||
// Dynamic, on demand connection?
|
||||
-(BOOL)isConnectionOnDemand;
|
||||
// Is user intervention required?
|
||||
-(BOOL)isInterventionRequired;
|
||||
|
||||
-(NetworkStatus)currentReachabilityStatus;
|
||||
-(SCNetworkReachabilityFlags)reachabilityFlags;
|
||||
-(NSString*)currentReachabilityString;
|
||||
-(NSString*)currentReachabilityFlags;
|
||||
|
||||
@end
|
||||
508
Pods/Reachability/Reachability.m
generated
508
Pods/Reachability/Reachability.m
generated
@ -1,508 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "Reachability.h"
|
||||
|
||||
#import <sys/socket.h>
|
||||
#import <netinet/in.h>
|
||||
#import <netinet6/in6.h>
|
||||
#import <arpa/inet.h>
|
||||
#import <ifaddrs.h>
|
||||
#import <netdb.h>
|
||||
|
||||
|
||||
NSString *const kReachabilityChangedNotification = @"kReachabilityChangedNotification";
|
||||
|
||||
|
||||
@interface Reachability ()
|
||||
|
||||
@property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef;
|
||||
@property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue;
|
||||
@property (nonatomic, strong) id reachabilityObject;
|
||||
|
||||
-(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags;
|
||||
-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags)
|
||||
{
|
||||
return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c",
|
||||
#if TARGET_OS_IPHONE
|
||||
(flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-',
|
||||
#else
|
||||
'X',
|
||||
#endif
|
||||
(flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-'];
|
||||
}
|
||||
|
||||
// Start listening for reachability notifications on the current run loop
|
||||
static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
|
||||
{
|
||||
#pragma unused (target)
|
||||
|
||||
Reachability *reachability = ((__bridge Reachability*)info);
|
||||
|
||||
// We probably don't need an autoreleasepool here, as GCD docs state each queue has its own autorelease pool,
|
||||
// but what the heck eh?
|
||||
@autoreleasepool
|
||||
{
|
||||
[reachability reachabilityChanged:flags];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@implementation Reachability
|
||||
|
||||
#pragma mark - Class Constructor Methods
|
||||
|
||||
+(instancetype)reachabilityWithHostName:(NSString*)hostname
|
||||
{
|
||||
return [Reachability reachabilityWithHostname:hostname];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithHostname:(NSString*)hostname
|
||||
{
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]);
|
||||
if (ref)
|
||||
{
|
||||
id reachability = [[self alloc] initWithReachabilityRef:ref];
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithAddress:(void *)hostAddress
|
||||
{
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress);
|
||||
if (ref)
|
||||
{
|
||||
id reachability = [[self alloc] initWithReachabilityRef:ref];
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityForInternetConnection
|
||||
{
|
||||
struct sockaddr_in zeroAddress;
|
||||
bzero(&zeroAddress, sizeof(zeroAddress));
|
||||
zeroAddress.sin_len = sizeof(zeroAddress);
|
||||
zeroAddress.sin_family = AF_INET;
|
||||
|
||||
return [self reachabilityWithAddress:&zeroAddress];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityForLocalWiFi
|
||||
{
|
||||
struct sockaddr_in localWifiAddress;
|
||||
bzero(&localWifiAddress, sizeof(localWifiAddress));
|
||||
localWifiAddress.sin_len = sizeof(localWifiAddress);
|
||||
localWifiAddress.sin_family = AF_INET;
|
||||
// IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
|
||||
localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM);
|
||||
|
||||
return [self reachabilityWithAddress:&localWifiAddress];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithURL:(NSURL*)url
|
||||
{
|
||||
id reachability;
|
||||
|
||||
NSString *host = url.host;
|
||||
BOOL isIpAddress = [self isIpAddress:host];
|
||||
|
||||
if (isIpAddress)
|
||||
{
|
||||
NSNumber *port = url.port ?: [url.scheme isEqualToString:@"https"] ? @(443) : @(80);
|
||||
|
||||
struct sockaddr_in address;
|
||||
address.sin_len = sizeof(address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons([port intValue]);
|
||||
address.sin_addr.s_addr = inet_addr([host UTF8String]);
|
||||
|
||||
reachability = [self reachabilityWithAddress:&address];
|
||||
}
|
||||
else
|
||||
{
|
||||
reachability = [self reachabilityWithHostname:host];
|
||||
}
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
+(BOOL)isIpAddress:(NSString*)host
|
||||
{
|
||||
struct in_addr pin;
|
||||
return 1 == inet_aton([host UTF8String], &pin);
|
||||
}
|
||||
|
||||
|
||||
// Initialization methods
|
||||
|
||||
-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
{
|
||||
self.reachableOnWWAN = YES;
|
||||
self.reachabilityRef = ref;
|
||||
|
||||
// We need to create a serial queue.
|
||||
// We allocate this once for the lifetime of the notifier.
|
||||
|
||||
self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)dealloc
|
||||
{
|
||||
[self stopNotifier];
|
||||
|
||||
if(self.reachabilityRef)
|
||||
{
|
||||
CFRelease(self.reachabilityRef);
|
||||
self.reachabilityRef = nil;
|
||||
}
|
||||
|
||||
self.reachableBlock = nil;
|
||||
self.unreachableBlock = nil;
|
||||
self.reachabilityBlock = nil;
|
||||
self.reachabilitySerialQueue = nil;
|
||||
}
|
||||
|
||||
#pragma mark - Notifier Methods
|
||||
|
||||
// Notifier
|
||||
// NOTE: This uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD
|
||||
// - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS.
|
||||
// INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you want)
|
||||
|
||||
-(BOOL)startNotifier
|
||||
{
|
||||
// allow start notifier to be called multiple times
|
||||
if(self.reachabilityObject && (self.reachabilityObject == self))
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
SCNetworkReachabilityContext context = { 0, NULL, NULL, NULL, NULL };
|
||||
context.info = (__bridge void *)self;
|
||||
|
||||
if(SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context))
|
||||
{
|
||||
// Set it as our reachability queue, which will retain the queue
|
||||
if(SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue))
|
||||
{
|
||||
// this should do a retain on ourself, so as long as we're in notifier mode we shouldn't disappear out from under ourselves
|
||||
// woah
|
||||
self.reachabilityObject = self;
|
||||
return YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
|
||||
// UH OH - FAILURE - stop any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
}
|
||||
|
||||
// if we get here we fail at the internet
|
||||
self.reachabilityObject = nil;
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(void)stopNotifier
|
||||
{
|
||||
// First stop, any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
|
||||
// Unregister target from the GCD serial dispatch queue.
|
||||
SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL);
|
||||
|
||||
self.reachabilityObject = nil;
|
||||
}
|
||||
|
||||
#pragma mark - reachability tests
|
||||
|
||||
// This is for the case where you flick the airplane mode;
|
||||
// you end up getting something like this:
|
||||
//Reachability: WR ct-----
|
||||
//Reachability: -- -------
|
||||
//Reachability: WR ct-----
|
||||
//Reachability: -- -------
|
||||
// We treat this as 4 UNREACHABLE triggers - really apple should do better than this
|
||||
|
||||
#define testcase (kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection)
|
||||
|
||||
-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags
|
||||
{
|
||||
BOOL connectionUP = YES;
|
||||
|
||||
if(!(flags & kSCNetworkReachabilityFlagsReachable))
|
||||
connectionUP = NO;
|
||||
|
||||
if( (flags & testcase) == testcase )
|
||||
connectionUP = NO;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if(flags & kSCNetworkReachabilityFlagsIsWWAN)
|
||||
{
|
||||
// We're on 3G.
|
||||
if(!self.reachableOnWWAN)
|
||||
{
|
||||
// We don't want to connect when on 3G.
|
||||
connectionUP = NO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return connectionUP;
|
||||
}
|
||||
|
||||
-(BOOL)isReachable
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
return NO;
|
||||
|
||||
return [self isReachableWithFlags:flags];
|
||||
}
|
||||
|
||||
-(BOOL)isReachableViaWWAN
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
// Check we're REACHABLE
|
||||
if(flags & kSCNetworkReachabilityFlagsReachable)
|
||||
{
|
||||
// Now, check we're on WWAN
|
||||
if(flags & kSCNetworkReachabilityFlagsIsWWAN)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(BOOL)isReachableViaWiFi
|
||||
{
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
// Check we're reachable
|
||||
if((flags & kSCNetworkReachabilityFlagsReachable))
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
// Check we're NOT on WWAN
|
||||
if((flags & kSCNetworkReachabilityFlagsIsWWAN))
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
#endif
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
-(BOOL)isConnectionRequired
|
||||
{
|
||||
return [self connectionRequired];
|
||||
}
|
||||
|
||||
-(BOOL)connectionRequired
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Dynamic, on demand connection?
|
||||
-(BOOL)isConnectionOnDemand
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand)));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Is user intervention required?
|
||||
-(BOOL)isInterventionRequired
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - reachability status stuff
|
||||
|
||||
-(NetworkStatus)currentReachabilityStatus
|
||||
{
|
||||
if([self isReachable])
|
||||
{
|
||||
if([self isReachableViaWiFi])
|
||||
return ReachableViaWiFi;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return ReachableViaWWAN;
|
||||
#endif
|
||||
}
|
||||
|
||||
return NotReachable;
|
||||
}
|
||||
|
||||
-(SCNetworkReachabilityFlags)reachabilityFlags
|
||||
{
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return flags;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
-(NSString*)currentReachabilityString
|
||||
{
|
||||
NetworkStatus temp = [self currentReachabilityStatus];
|
||||
|
||||
if(temp == ReachableViaWWAN)
|
||||
{
|
||||
// Updated for the fact that we have CDMA phones now!
|
||||
return NSLocalizedString(@"Cellular", @"");
|
||||
}
|
||||
if (temp == ReachableViaWiFi)
|
||||
{
|
||||
return NSLocalizedString(@"WiFi", @"");
|
||||
}
|
||||
|
||||
return NSLocalizedString(@"No Connection", @"");
|
||||
}
|
||||
|
||||
-(NSString*)currentReachabilityFlags
|
||||
{
|
||||
return reachabilityFlags([self reachabilityFlags]);
|
||||
}
|
||||
|
||||
#pragma mark - Callback function calls this method
|
||||
|
||||
-(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags
|
||||
{
|
||||
if([self isReachableWithFlags:flags])
|
||||
{
|
||||
if(self.reachableBlock)
|
||||
{
|
||||
self.reachableBlock(self);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(self.unreachableBlock)
|
||||
{
|
||||
self.unreachableBlock(self);
|
||||
}
|
||||
}
|
||||
|
||||
if(self.reachabilityBlock)
|
||||
{
|
||||
self.reachabilityBlock(self, flags);
|
||||
}
|
||||
|
||||
// this makes sure the change notification happens on the MAIN THREAD
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kReachabilityChangedNotification
|
||||
object:self];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Debug Description
|
||||
|
||||
- (NSString *) description
|
||||
{
|
||||
NSString *description = [NSString stringWithFormat:@"<%@: %p (%@)>",
|
||||
NSStringFromClass([self class]), self, [self currentReachabilityFlags]];
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -1,5 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_FreeStreamer : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_FreeStreamer
|
||||
@end
|
||||
@ -1,12 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#import "FSAudioController.h"
|
||||
#import "FSAudioStream.h"
|
||||
#import "FSCheckContentTypeRequest.h"
|
||||
#import "FSParsePlaylistRequest.h"
|
||||
#import "FSParseRssPodcastFeedRequest.h"
|
||||
#import "FSPlaylistItem.h"
|
||||
#import "FSXMLHttpRequest.h"
|
||||
|
||||
FOUNDATION_EXPORT double FreeStreamerVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char FreeStreamerVersionString[];
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FreeStreamer
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Reachability"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) $(SDKROOT)/usr/include/libxml2
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"xml2" -framework "AVFoundation" -framework "AudioToolbox" -framework "CFNetwork" -framework "MediaPlayer" -framework "Reachability" -framework "SystemConfiguration"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/FreeStreamer
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@ -1,6 +0,0 @@
|
||||
framework module FreeStreamer {
|
||||
umbrella header "FreeStreamer-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FreeStreamer
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Reachability"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) $(SDKROOT)/usr/include/libxml2
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"xml2" -framework "AVFoundation" -framework "AudioToolbox" -framework "CFNetwork" -framework "MediaPlayer" -framework "Reachability" -framework "SystemConfiguration"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/FreeStreamer
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@ -47,63 +47,6 @@ Contact details
|
||||
Email: wk.katunin@gmail.com
|
||||
Site: https://ru.linkedin.com/in/pavelkatunin
|
||||
|
||||
## FreeStreamer
|
||||
|
||||
Copyright (c) 2011-2018 Matias Muhonen <mmu@iki.fi> 穆马帝
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
The FreeStreamer framework bundles Reachability which is licensed under the following
|
||||
license:
|
||||
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
## IQKeyboardManagerSwift
|
||||
|
||||
MIT License
|
||||
@ -228,20 +171,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
## Reachability
|
||||
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
## SVProgressHUD
|
||||
|
||||
MIT License
|
||||
|
||||
@ -70,69 +70,6 @@ Site: https://ru.linkedin.com/in/pavelkatunin</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2011-2018 Matias Muhonen <mmu@iki.fi> 穆马帝
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
The FreeStreamer framework bundles Reachability which is licensed under the following
|
||||
license:
|
||||
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>BSD</string>
|
||||
<key>Title</key>
|
||||
<string>FreeStreamer</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>MIT License
|
||||
@ -287,26 +224,6 @@ THE SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>BSD</string>
|
||||
<key>Title</key>
|
||||
<string>Reachability</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>MIT License
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
${PODS_ROOT}/Target Support Files/Pods-relax.offline.mp3.music/Pods-relax.offline.mp3.music-frameworks.sh
|
||||
${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework
|
||||
${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework
|
||||
${BUILT_PRODUCTS_DIR}/FreeStreamer/FreeStreamer.framework
|
||||
${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework
|
||||
${BUILT_PRODUCTS_DIR}/JXPagingView/JXPagingView.framework
|
||||
${BUILT_PRODUCTS_DIR}/JXSegmentedView/JXSegmentedView.framework
|
||||
${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework
|
||||
${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework
|
||||
${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework
|
||||
${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework
|
||||
${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
|
||||
${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FreeStreamer.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JXPagingView.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JXSegmentedView.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MJRefresh.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftDate.framework
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
${PODS_ROOT}/Target Support Files/Pods-relax.offline.mp3.music/Pods-relax.offline.mp3.music-frameworks.sh
|
||||
${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework
|
||||
${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework
|
||||
${BUILT_PRODUCTS_DIR}/FreeStreamer/FreeStreamer.framework
|
||||
${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework
|
||||
${BUILT_PRODUCTS_DIR}/JXPagingView/JXPagingView.framework
|
||||
${BUILT_PRODUCTS_DIR}/JXSegmentedView/JXSegmentedView.framework
|
||||
${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework
|
||||
${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework
|
||||
${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework
|
||||
${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework
|
||||
${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
|
||||
${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FreeStreamer.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JXPagingView.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JXSegmentedView.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MJRefresh.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftDate.framework
|
||||
|
||||
@ -178,13 +178,11 @@ code_sign_if_enabled() {
|
||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/FreeStreamer/FreeStreamer.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/JXPagingView/JXPagingView.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/JXSegmentedView/JXSegmentedView.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework"
|
||||
@ -193,13 +191,11 @@ fi
|
||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/FreeStreamer/FreeStreamer.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/JXPagingView/JXPagingView.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/JXSegmentedView/JXSegmentedView.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework"
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
${PODS_ROOT}/Target Support Files/Pods-relax.offline.mp3.music/Pods-relax.offline.mp3.music-resources.sh
|
||||
${PODS_CONFIGURATION_BUILD_DIR}/Google-Mobile-Ads-SDK/GoogleMobileAdsResources.bundle
|
||||
${PODS_CONFIGURATION_BUILD_DIR}/GoogleUserMessagingPlatform/UserMessagingPlatformResources.bundle
|
||||
@ -0,0 +1,2 @@
|
||||
${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMobileAdsResources.bundle
|
||||
${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/UserMessagingPlatformResources.bundle
|
||||
@ -0,0 +1,3 @@
|
||||
${PODS_ROOT}/Target Support Files/Pods-relax.offline.mp3.music/Pods-relax.offline.mp3.music-resources.sh
|
||||
${PODS_CONFIGURATION_BUILD_DIR}/Google-Mobile-Ads-SDK/GoogleMobileAdsResources.bundle
|
||||
${PODS_CONFIGURATION_BUILD_DIR}/GoogleUserMessagingPlatform/UserMessagingPlatformResources.bundle
|
||||
@ -0,0 +1,2 @@
|
||||
${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMobileAdsResources.bundle
|
||||
${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/UserMessagingPlatformResources.bundle
|
||||
131
Pods/Target Support Files/Pods-relax.offline.mp3.music/Pods-relax.offline.mp3.music-resources.sh
generated
Executable file
131
Pods/Target Support Files/Pods-relax.offline.mp3.music/Pods-relax.offline.mp3.music-resources.sh
generated
Executable file
@ -0,0 +1,131 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then
|
||||
# If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy
|
||||
# resources to, so exit 0 (signalling the script phase was successful).
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
|
||||
RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt
|
||||
> "$RESOURCES_TO_COPY"
|
||||
|
||||
XCASSET_FILES=()
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||
|
||||
case "${TARGETED_DEVICE_FAMILY:-}" in
|
||||
1,2)
|
||||
TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone"
|
||||
;;
|
||||
1)
|
||||
TARGET_DEVICE_ARGS="--target-device iphone"
|
||||
;;
|
||||
2)
|
||||
TARGET_DEVICE_ARGS="--target-device ipad"
|
||||
;;
|
||||
3)
|
||||
TARGET_DEVICE_ARGS="--target-device tv"
|
||||
;;
|
||||
4)
|
||||
TARGET_DEVICE_ARGS="--target-device watch"
|
||||
;;
|
||||
*)
|
||||
TARGET_DEVICE_ARGS="--target-device mac"
|
||||
;;
|
||||
esac
|
||||
|
||||
install_resource()
|
||||
{
|
||||
if [[ "$1" = /* ]] ; then
|
||||
RESOURCE_PATH="$1"
|
||||
else
|
||||
RESOURCE_PATH="${PODS_ROOT}/$1"
|
||||
fi
|
||||
if [[ ! -e "$RESOURCE_PATH" ]] ; then
|
||||
cat << EOM
|
||||
error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script.
|
||||
EOM
|
||||
exit 1
|
||||
fi
|
||||
case $RESOURCE_PATH in
|
||||
*.storyboard)
|
||||
echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true
|
||||
ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS}
|
||||
;;
|
||||
*.xib)
|
||||
echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true
|
||||
ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS}
|
||||
;;
|
||||
*.framework)
|
||||
echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true
|
||||
mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
;;
|
||||
*.xcdatamodel)
|
||||
echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true
|
||||
xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom"
|
||||
;;
|
||||
*.xcdatamodeld)
|
||||
echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true
|
||||
xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd"
|
||||
;;
|
||||
*.xcmappingmodel)
|
||||
echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true
|
||||
xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm"
|
||||
;;
|
||||
*.xcassets)
|
||||
ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH"
|
||||
XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE")
|
||||
;;
|
||||
*)
|
||||
echo "$RESOURCE_PATH" || true
|
||||
echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/Google-Mobile-Ads-SDK/GoogleMobileAdsResources.bundle"
|
||||
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUserMessagingPlatform/UserMessagingPlatformResources.bundle"
|
||||
fi
|
||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/Google-Mobile-Ads-SDK/GoogleMobileAdsResources.bundle"
|
||||
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUserMessagingPlatform/UserMessagingPlatformResources.bundle"
|
||||
fi
|
||||
|
||||
mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then
|
||||
mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
fi
|
||||
rm -f "$RESOURCES_TO_COPY"
|
||||
|
||||
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ]
|
||||
then
|
||||
# Find all other xcassets (this unfortunately includes those of path pods and other targets).
|
||||
OTHER_XCASSETS=$(find -L "$PWD" -iname "*.xcassets" -type d)
|
||||
while read line; do
|
||||
if [[ $line != "${PODS_ROOT}*" ]]; then
|
||||
XCASSET_FILES+=("$line")
|
||||
fi
|
||||
done <<<"$OTHER_XCASSETS"
|
||||
|
||||
if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then
|
||||
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
else
|
||||
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist"
|
||||
fi
|
||||
fi
|
||||
@ -1,11 +1,11 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton" "${PODS_CONFIGURATION_BUILD_DIR}/FreeStreamer" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton/DownloadButton.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/FreeStreamer/FreeStreamer.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView/JXPagingView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView/JXSegmentedView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate/SwiftDate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel/Tiercel.framework/Headers" $(SDKROOT)/usr/include/libxml2
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton/DownloadButton.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView/JXPagingView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView/JXSegmentedView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate/SwiftDate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel/Tiercel.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"swiftCoreGraphics" -l"xml2" -framework "AVFoundation" -framework "Accelerate" -framework "Alamofire" -framework "AudioToolbox" -framework "CFNetwork" -framework "CoreGraphics" -framework "DownloadButton" -framework "Foundation" -framework "FreeStreamer" -framework "IQKeyboardManagerSwift" -framework "JXPagingView" -framework "JXSegmentedView" -framework "Kingfisher" -framework "MJRefresh" -framework "MediaPlayer" -framework "QuartzCore" -framework "Reachability" -framework "SVProgressHUD" -framework "SnapKit" -framework "SwiftDate" -framework "SystemConfiguration" -framework "Tiercel" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI"
|
||||
OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "CoreGraphics" -framework "DownloadButton" -framework "Foundation" -framework "IQKeyboardManagerSwift" -framework "JXPagingView" -framework "JXSegmentedView" -framework "Kingfisher" -framework "MJRefresh" -framework "QuartzCore" -framework "SVProgressHUD" -framework "SnapKit" -framework "SwiftDate" -framework "Tiercel" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton" "${PODS_CONFIGURATION_BUILD_DIR}/FreeStreamer" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton/DownloadButton.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/FreeStreamer/FreeStreamer.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView/JXPagingView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView/JXSegmentedView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate/SwiftDate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel/Tiercel.framework/Headers" $(SDKROOT)/usr/include/libxml2
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DownloadButton/DownloadButton.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView/JXPagingView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JXSegmentedView/JXSegmentedView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate/SwiftDate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Tiercel/Tiercel.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"swiftCoreGraphics" -l"xml2" -framework "AVFoundation" -framework "Accelerate" -framework "Alamofire" -framework "AudioToolbox" -framework "CFNetwork" -framework "CoreGraphics" -framework "DownloadButton" -framework "Foundation" -framework "FreeStreamer" -framework "IQKeyboardManagerSwift" -framework "JXPagingView" -framework "JXSegmentedView" -framework "Kingfisher" -framework "MJRefresh" -framework "MediaPlayer" -framework "QuartzCore" -framework "Reachability" -framework "SVProgressHUD" -framework "SnapKit" -framework "SwiftDate" -framework "SystemConfiguration" -framework "Tiercel" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI"
|
||||
OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "CoreGraphics" -framework "DownloadButton" -framework "Foundation" -framework "IQKeyboardManagerSwift" -framework "JXPagingView" -framework "JXSegmentedView" -framework "Kingfisher" -framework "MJRefresh" -framework "QuartzCore" -framework "SVProgressHUD" -framework "SnapKit" -framework "SwiftDate" -framework "Tiercel" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.7.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -1,5 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_Reachability : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_Reachability
|
||||
@end
|
||||
@ -1,12 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#import "Reachability.h"
|
||||
|
||||
FOUNDATION_EXPORT double ReachabilityVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[];
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Reachability
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_LDFLAGS = $(inherited) -framework "SystemConfiguration"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Reachability
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@ -1,6 +0,0 @@
|
||||
framework module Reachability {
|
||||
umbrella header "Reachability-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Reachability
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_LDFLAGS = $(inherited) -framework "SystemConfiguration"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Reachability
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.7.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
46C4D5B84AF3CA6292152C70 /* Pods_relax_offline_mp3_music.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DB1E9160CFEC3A0E35F11BF /* Pods_relax_offline_mp3_music.framework */; };
|
||||
CB000F442C12AD2300B0FC0A /* GoogleMobileAds in Frameworks */ = {isa = PBXBuildFile; productRef = CB000F432C12AD2300B0FC0A /* GoogleMobileAds */; };
|
||||
CBAFCAE62C0A10500054500E /* MP_BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAFC9F22C0A10500054500E /* MP_BaseViewController.swift */; };
|
||||
CBAFCAE72C0A10500054500E /* MP_LunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAFC9F32C0A10500054500E /* MP_LunchViewController.swift */; };
|
||||
CBAFCAE82C0A10500054500E /* MP_LunchViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CBAFC9F42C0A10500054500E /* MP_LunchViewController.xib */; };
|
||||
@ -430,6 +431,7 @@
|
||||
files = (
|
||||
CBAFCBAD2C0A10DA0054500E /* FirebaseCrashlytics in Frameworks */,
|
||||
CBAFCBAB2C0A10DA0054500E /* FirebaseAnalytics in Frameworks */,
|
||||
CB000F442C12AD2300B0FC0A /* GoogleMobileAds in Frameworks */,
|
||||
46C4D5B84AF3CA6292152C70 /* Pods_relax_offline_mp3_music.framework in Frameworks */,
|
||||
CBAFCBAF2C0A10DA0054500E /* FirebaseRemoteConfig in Frameworks */,
|
||||
);
|
||||
@ -932,10 +934,10 @@
|
||||
CBAFCAC82C0A10500054500E /* Home(音乐资源列表) */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CBAFCAC22C0A10500054500E /* MPSideA_CountTimerViewController.swift */,
|
||||
CBAFCAC32C0A10500054500E /* MPSideA_CountTimerViewController.xib */,
|
||||
CBAFCAC42C0A10500054500E /* MPSideA_HomeViewController.swift */,
|
||||
CBAFCAC52C0A10500054500E /* MPSideA_HomeViewController.xib */,
|
||||
CBAFCAC22C0A10500054500E /* MPSideA_CountTimerViewController.swift */,
|
||||
CBAFCAC32C0A10500054500E /* MPSideA_CountTimerViewController.xib */,
|
||||
CBAFCAC62C0A10500054500E /* MPSideA_PlayerViewController.swift */,
|
||||
CBAFCAC72C0A10500054500E /* MPSideA_PlayerViewController.xib */,
|
||||
);
|
||||
@ -1089,6 +1091,7 @@
|
||||
CBAFCBAA2C0A10DA0054500E /* FirebaseAnalytics */,
|
||||
CBAFCBAC2C0A10DA0054500E /* FirebaseCrashlytics */,
|
||||
CBAFCBAE2C0A10DA0054500E /* FirebaseRemoteConfig */,
|
||||
CB000F432C12AD2300B0FC0A /* GoogleMobileAds */,
|
||||
);
|
||||
productName = relax.offline.mp3.music;
|
||||
productReference = CBC2D6E82BFDF3D700E17703 /* relax.offline.mp3.music.app */;
|
||||
@ -1120,6 +1123,7 @@
|
||||
mainGroup = CBC2D6DF2BFDF3D700E17703;
|
||||
packageReferences = (
|
||||
CBAFCBA92C0A10DA0054500E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
|
||||
CB000F422C12AD2300B0FC0A /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */,
|
||||
);
|
||||
productRefGroup = CBC2D6E92BFDF3D700E17703 /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -1559,7 +1563,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1.0.4.1;
|
||||
CURRENT_PROJECT_VERSION = 1.0.5.1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = T93S37G27F;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -1578,7 +1582,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MARKETING_VERSION = 1.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = relax.offline.mp3.music;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -1600,7 +1604,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1.0.4.1;
|
||||
CURRENT_PROJECT_VERSION = 1.0.5.1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = T93S37G27F;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -1619,7 +1623,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MARKETING_VERSION = 1.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = relax.offline.mp3.music;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -1657,6 +1661,14 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
CB000F422C12AD2300B0FC0A /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/googleads/swift-package-manager-google-mobile-ads.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 11.5.0;
|
||||
};
|
||||
};
|
||||
CBAFCBA92C0A10DA0054500E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
|
||||
@ -1668,6 +1680,11 @@
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
CB000F432C12AD2300B0FC0A /* GoogleMobileAds */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = CB000F422C12AD2300B0FC0A /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */;
|
||||
productName = GoogleMobileAds;
|
||||
};
|
||||
CBAFCBAA2C0A10DA0054500E /* FirebaseAnalytics */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = CBAFCBA92C0A10DA0054500E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
|
||||
"originHash" : "03651f2e356682d9ff3c19f48721f41adee49ee676f5dbdcab8120359ace4980",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
@ -109,6 +109,24 @@
|
||||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-package-manager-google-mobile-ads",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads.git",
|
||||
"state" : {
|
||||
"revision" : "746253bf8803826e4de14c9e619739733ea8fb3b",
|
||||
"version" : "11.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-package-manager-google-user-messaging-platform",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git",
|
||||
"state" : {
|
||||
"revision" : "9b68aa69fb508f0274853e226c734151a973c7b7",
|
||||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-protobuf",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@ -10,15 +10,17 @@ import CoreData
|
||||
import AVFoundation
|
||||
import Alamofire
|
||||
import Tiercel
|
||||
import FirebaseCore
|
||||
import Firebase
|
||||
@_exported import IQKeyboardManagerSwift
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
//当前页面状态
|
||||
private var showStatus:Bool!
|
||||
// 用来保存由系统提供的完成处理器
|
||||
var backgroundSessionCompletionHandler: (() -> Void)?
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
//生成一个FireBase实例
|
||||
//FireBase初始化
|
||||
FirebaseApp.configure()
|
||||
//启动前销毁所有的下载任务
|
||||
MP_DownloadManager.shared.cancelAllTasksIfNeeded()
|
||||
@ -39,7 +41,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
MP_DownloadManager.shared.session.completionHandler = completionHandler
|
||||
}
|
||||
}
|
||||
|
||||
//设置播放器会话状态
|
||||
private func setAudioSupport(){
|
||||
//设置会话
|
||||
@ -71,6 +72,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
///跳转A面
|
||||
func switch_aSide() {
|
||||
guard showStatus != false else {
|
||||
return
|
||||
}
|
||||
let tabBarVC = MPSideA_TabBarController()
|
||||
//动画设置
|
||||
let transtition = CATransition()
|
||||
@ -79,9 +83,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
window?.layer.add(transtition, forKey: "aSide.easeOut")
|
||||
window?.rootViewController = tabBarVC
|
||||
window?.makeKeyAndVisible()
|
||||
showStatus = false
|
||||
}
|
||||
//跳转b面
|
||||
func switch_positive() {
|
||||
guard showStatus != true else {
|
||||
return
|
||||
}
|
||||
//销毁A面所有内容
|
||||
MPSideA_MediaCenterManager.shared.destroySideA()
|
||||
MPSideA_VolumeManager.shared.destroySideA()
|
||||
let tabBarVC = MPPositive_TabBarController()
|
||||
//动画设置
|
||||
let transtition = CATransition()
|
||||
@ -90,6 +101,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
window?.layer.add(transtition, forKey: "positive.easeOut")
|
||||
window?.rootViewController = tabBarVC
|
||||
window?.makeKeyAndVisible()
|
||||
showStatus = true
|
||||
}
|
||||
// MARK: - Core Data stack
|
||||
lazy var persistentContainer: NSPersistentContainer = {
|
||||
|
||||
@ -16,11 +16,13 @@ class MP_LunchViewController: UIViewController {
|
||||
//帧计时器
|
||||
private var timer:CADisplayLink!
|
||||
//最大计时值
|
||||
private lazy var maxTimes:TimeInterval = 6
|
||||
private lazy var maxTimes:TimeInterval = 7
|
||||
//当前计时值
|
||||
private lazy var currentTimes:TimeInterval = 0
|
||||
//计时器结束事件
|
||||
private var completionBlock:(() -> Void)?
|
||||
//是否执行过A/B面检测
|
||||
private var isBeen:Bool = false
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .init(hex: "#000000")
|
||||
@ -31,20 +33,9 @@ class MP_LunchViewController: UIViewController {
|
||||
timer.add(to: RunLoop.current, forMode: .common)
|
||||
//启动计时器
|
||||
timer.isPaused = false
|
||||
// self.completionBlock = {
|
||||
// DispatchQueue.main.async {
|
||||
// [weak self] in
|
||||
// guard let self = self else {return}
|
||||
// //停止计时器
|
||||
// timer.isPaused = true
|
||||
// //加载完毕,判断并跳转
|
||||
// accessAppdelegate.switch_positive()
|
||||
// //获取首页
|
||||
// MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
deinit {
|
||||
timer.isPaused = true
|
||||
//销毁计时器
|
||||
timer.invalidate()
|
||||
timer = nil
|
||||
@ -52,16 +43,9 @@ class MP_LunchViewController: UIViewController {
|
||||
}
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
// 请求跟踪授权
|
||||
requestTrackingAuthorization { idfa in
|
||||
// if let idfa = idfa {
|
||||
// print("IDFA: \(idfa)")
|
||||
// StartManager.shared.idfaid = idfa
|
||||
// print("Stored IDFA: \(StartManager.shared.idfaid ?? "N/A")")
|
||||
// } else {
|
||||
// print("IDFA is not available or tracking authorization denied.")
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,41 +66,20 @@ class MP_LunchViewController: UIViewController {
|
||||
//设置进度条
|
||||
progressView.setProgress(value)
|
||||
}
|
||||
//执行A/B面检测
|
||||
if isBeen == false {
|
||||
//修改值避免一直检测
|
||||
isBeen = true
|
||||
switchAOrBAction()
|
||||
}
|
||||
}else {
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
MP_AnalyticsManager.shared.getOpenStatus { [weak self] open in
|
||||
guard let self = self else {return}
|
||||
if open {
|
||||
//根据ip值确定进入那个页面
|
||||
MP_NetWorkManager.shared.requestIPInfo { statu in
|
||||
if statu == true {
|
||||
//允许进入b面
|
||||
print("BLog")
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_positive()
|
||||
//获取首页
|
||||
MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists()
|
||||
}
|
||||
}else {
|
||||
print("ALog")
|
||||
//打开A面
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_aSide()
|
||||
}
|
||||
}
|
||||
}
|
||||
guard MP_NetWorkManager.shared.netWorkStatu != .reachable else {
|
||||
if completionBlock != nil {
|
||||
completionBlock!()
|
||||
}else {
|
||||
//完成闭包没有配置完成,直接前往A面
|
||||
print("ALog")
|
||||
//打开A面
|
||||
DispatchQueue.main.async {
|
||||
@ -128,6 +91,142 @@ class MP_LunchViewController: UIViewController {
|
||||
accessAppdelegate.switch_aSide()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
//进度条加载结束,执行完成闭包
|
||||
if completionBlock != nil {
|
||||
completionBlock!()
|
||||
}else {
|
||||
//完成闭包没有配置完成,直接前往A面
|
||||
print("ALog")
|
||||
//打开A面
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_aSide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//a/b面开关判断
|
||||
private func switchAOrBAction() {
|
||||
//首先检测之前是否进入过B面
|
||||
guard UserDefaults.standard.bool(forKey: "MP_Into_B") != true else {
|
||||
//进入过B面
|
||||
self.completionBlock = {
|
||||
//允许进入b面
|
||||
print("BLog")
|
||||
//保存进入B面的记录,下次加载直接进入B面
|
||||
UserDefaults.standard.set(true, forKey: "MP_Into_B")
|
||||
//获取B面首页数据
|
||||
MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists()
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_positive()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
MP_NetWorkManager.shared.performTaskNetWrokAvailable {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//获取B面首页数据
|
||||
MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists()
|
||||
//进行开关检测
|
||||
MP_AnalyticsManager.shared.getOpenStatus { [weak self] open in
|
||||
guard let self = self else {return}
|
||||
if open {
|
||||
//开关检测为通过,在检测IP地址
|
||||
//根据ip值确定进入那个页面
|
||||
MP_NetWorkManager.shared.requestIPInfo { statu in
|
||||
if statu == true {
|
||||
//允许进入b面
|
||||
print("BLog")
|
||||
//保存进入B面的记录,下次加载直接进入B面
|
||||
UserDefaults.standard.set(true, forKey: "MP_Into_B")
|
||||
//判断当前时间节点是否已经超时
|
||||
if self.maxTimes > self.currentTimes {
|
||||
//未超时,完成包处理
|
||||
self.completionBlock = {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_positive()
|
||||
}
|
||||
}
|
||||
}else {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_positive()
|
||||
}
|
||||
}
|
||||
}else {
|
||||
print("ALog")
|
||||
//打开A面
|
||||
if self.maxTimes > self.currentTimes {
|
||||
self.completionBlock = {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_aSide()
|
||||
}
|
||||
}
|
||||
}else {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_aSide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}else {
|
||||
print("ALog")
|
||||
//打开A面
|
||||
if self.maxTimes > self.currentTimes {
|
||||
self.completionBlock = {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_aSide()
|
||||
}
|
||||
}
|
||||
}else {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//停止计时器
|
||||
timer.isPaused = true
|
||||
//加载完毕,判断并跳转
|
||||
accessAppdelegate.switch_aSide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,21 +84,38 @@ class MPSideA_MediaCenterManager {
|
||||
static let shared = MPSideA_MediaCenterManager()
|
||||
//MARK: - 各项工具
|
||||
//音乐播放器
|
||||
private var player:AVPlayer?
|
||||
var player:AVPlayer?
|
||||
//远程控制中心
|
||||
private var center:MPRemoteCommandCenter?
|
||||
var center:MPRemoteCommandCenter?
|
||||
//GCD倒计时器
|
||||
private var countTimer: DispatchSourceTimer?
|
||||
var countTimer: DispatchSourceTimer?
|
||||
//监听器(需要获取麦克风权限),通过监听器实时获取周遭分贝
|
||||
private var monitor:AVAudioRecorder?
|
||||
var monitor:AVAudioRecorder?
|
||||
//监听器设置参数字典
|
||||
private var monitorSetingsDic:[String : Any]?
|
||||
var monitorSetingsDic:[String : Any]?
|
||||
//GCD监听计时器
|
||||
private var monitorTimer: DispatchSourceTimer?
|
||||
var monitorTimer: DispatchSourceTimer?
|
||||
//A面销毁
|
||||
func destroySideA() {
|
||||
//解除监听
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
//释放播放器
|
||||
player = nil
|
||||
//释放倒计时器
|
||||
countTimer?.cancel()
|
||||
countTimer = nil
|
||||
//释放监听器
|
||||
monitor = nil
|
||||
monitorTimer?.cancel()
|
||||
monitorTimer = nil
|
||||
center?.playCommand.removeTarget(self)
|
||||
center?.pauseCommand.removeTarget(self)
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
}
|
||||
|
||||
//MARK: - 实体与状态值
|
||||
///音乐实体
|
||||
private var music:MPSideA_MusicModel?
|
||||
var music:MPSideA_MusicModel?
|
||||
///获取音乐实体
|
||||
func getMusic() -> MPSideA_MusicModel?{
|
||||
return music
|
||||
|
||||
@ -78,4 +78,10 @@ class MPSideA_VolumeManager:NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
//A面销毁
|
||||
func destroySideA() {
|
||||
//移除所有监听
|
||||
AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
|
||||
destroyVolume()
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,15 +22,15 @@ class MP_NetWorkManager: NSObject {
|
||||
private lazy var MPSession:Session = {
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
//最多同时执行4条
|
||||
configuration.timeoutIntervalForRequest = 30
|
||||
configuration.timeoutIntervalForResource = 30
|
||||
configuration.timeoutIntervalForRequest = 12
|
||||
configuration.timeoutIntervalForResource = 12
|
||||
return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier())
|
||||
}()
|
||||
///播放资源会话实例
|
||||
private lazy var PlayerSeesion:Session = {
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.timeoutIntervalForRequest = 30
|
||||
configuration.timeoutIntervalForResource = 30
|
||||
configuration.timeoutIntervalForRequest = 12
|
||||
configuration.timeoutIntervalForResource = 12
|
||||
//最多同时执行4条
|
||||
configuration.httpMaximumConnectionsPerHost = 4
|
||||
return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier())
|
||||
@ -81,8 +81,6 @@ class MP_NetWorkManager: NSObject {
|
||||
case reachable = "网络可用"
|
||||
}
|
||||
///网络监听器
|
||||
private let reachabilityManager:NetworkReachabilityManager = NetworkReachabilityManager(host: "https://music.youtube.com/")!
|
||||
///网络监听器
|
||||
private var monitor:NWPathMonitor
|
||||
///对监听器的判断
|
||||
private var isReach:Bool = false
|
||||
@ -107,8 +105,6 @@ class MP_NetWorkManager: NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
//避免连续切换网络状态计时器
|
||||
private var debounceTimer: Timer?
|
||||
//MARK: - 固定参数
|
||||
//访问数据(首次首页预览时获得)
|
||||
private var visitorData:String?
|
||||
@ -155,7 +151,11 @@ class MP_NetWorkManager: NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
//MARK: - GCD队列
|
||||
//MARK: - GCD队列任务
|
||||
///网络检测队列
|
||||
private let MPNetWorkqueue = DispatchQueue(label: "MPNetWorkManager")
|
||||
///A/B任务处理队伍
|
||||
private var executionQueue: [() -> Void] = []
|
||||
///串行队列-预览
|
||||
private var browseQueque:DispatchQueue?
|
||||
///并发队列-单曲资源预加载
|
||||
@ -167,13 +167,11 @@ class MP_NetWorkManager: NSObject {
|
||||
private override init() {
|
||||
self.monitor = NWPathMonitor()
|
||||
super.init()
|
||||
|
||||
}
|
||||
|
||||
//MARK: - 网络情况
|
||||
///检查网络状况
|
||||
func requestNetworkPermission(oberve:UIViewController, completeHanlder:ActionBlock?) {
|
||||
let monitor = NWPathMonitor()
|
||||
switch netWorkStatu {
|
||||
case .reachable:
|
||||
DispatchQueue.main.async {
|
||||
@ -212,23 +210,7 @@ class MP_NetWorkManager: NSObject {
|
||||
return
|
||||
}
|
||||
isReach = true
|
||||
// //通过ping节点确认是否能执行网络请求
|
||||
// reachabilityManager.startListening(onQueue: .main, onUpdatePerforming: { [weak self] status in
|
||||
// guard let self = self else {return}
|
||||
// var newStatu:NetWorkStatus = .unknown
|
||||
// switch status {
|
||||
// case .unknown://未知状况
|
||||
// newStatu = .unknown
|
||||
// case .notReachable://网络不可用
|
||||
// newStatu = .notReachable
|
||||
// case .reachable(.ethernetOrWiFi), .reachable(.cellular)://网络可用,且做出了分类
|
||||
// newStatu = .reachable
|
||||
// }
|
||||
// // 去抖处理,确保状态变化稳定后再更新
|
||||
// self.handleDebouncedNetworkStatusChange(newStatu)
|
||||
// })
|
||||
let queue = DispatchQueue(label: "MPNetWorkManager")
|
||||
monitor.start(queue: queue)
|
||||
monitor.start(queue: MPNetWorkqueue)
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
self?.updateNetworkStatus(path)
|
||||
}
|
||||
@ -236,25 +218,27 @@ class MP_NetWorkManager: NSObject {
|
||||
private func updateNetworkStatus(_ path: NWPath) {
|
||||
if path.status == .satisfied {
|
||||
netWorkStatu = .reachable
|
||||
executePendingTasks()
|
||||
} else {
|
||||
netWorkStatu = .notReachable
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening() {
|
||||
reachabilityManager.stopListening()
|
||||
isReach = false
|
||||
//AB面检测队列任务
|
||||
func performTaskNetWrokAvailable(_ task: @escaping() -> Void) {
|
||||
//根据当前网络情况决定是执行任务,还是将任务放置到队列
|
||||
if netWorkStatu == .reachable {
|
||||
//网络可以执行任务
|
||||
task()
|
||||
}else {
|
||||
//网络不可执行任务,放入队列
|
||||
executionQueue.append(task)
|
||||
}
|
||||
}
|
||||
private func handleDebouncedNetworkStatusChange(_ newStatus: NetWorkStatus) {
|
||||
// 取消之前的去抖操作
|
||||
debounceTimer?.invalidate()
|
||||
// 设置新的去抖操作
|
||||
debounceTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
// 更新状态时只在去抖时间窗口后处理变化
|
||||
if newStatus != self.netWorkStatu {
|
||||
self.netWorkStatu = newStatus
|
||||
}
|
||||
//当网络恢复可用后立刻处理队列任务
|
||||
private func executePendingTasks() {
|
||||
while !executionQueue.isEmpty {
|
||||
let task = executionQueue.removeFirst()
|
||||
task()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2010,7 +1994,7 @@ class MP_CustomRetrier: RequestInterceptor {
|
||||
// 追踪重试次数的字典
|
||||
private var retryCounts: [String: Int] = [:]
|
||||
// 最大重试次数
|
||||
private let maxRetryCount: Int = 3
|
||||
private let maxRetryCount: Int = 2
|
||||
// 重试间隔时间
|
||||
private let retryInterval: TimeInterval = 1.0
|
||||
//重试策略
|
||||
|
||||
@ -9,7 +9,6 @@ import UIKit
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
import AVKit
|
||||
import FreeStreamer
|
||||
import Kingfisher
|
||||
///播放器播放状态
|
||||
enum MP_PlayerStateType:Int {
|
||||
@ -620,7 +619,7 @@ class MP_PlayerManager:NSObject{
|
||||
//播放
|
||||
center!.playCommand.addTarget(handler: { [weak self] (event) in
|
||||
guard let self = self else { return .noActionableNowPlayingItem}
|
||||
if loadPlayer.currentVideo != nil {
|
||||
if loadPlayer.currentVideo != nil && playState == .Pause {
|
||||
resume()
|
||||
return .success
|
||||
}else {
|
||||
@ -630,7 +629,7 @@ class MP_PlayerManager:NSObject{
|
||||
//暂停
|
||||
center!.pauseCommand.addTarget(handler: { [weak self] (event) in
|
||||
guard let self = self else { return .noActionableNowPlayingItem}
|
||||
if loadPlayer.currentVideo != nil {
|
||||
if loadPlayer.currentVideo != nil && playState == .Playing {
|
||||
pause()
|
||||
return .success
|
||||
}else {
|
||||
@ -640,7 +639,7 @@ class MP_PlayerManager:NSObject{
|
||||
//上一首
|
||||
center!.previousTrackCommand.addTarget { [weak self] (event) in
|
||||
guard let self = self else { return .noActionableNowPlayingItem}
|
||||
if loadPlayer.currentVideo != nil {
|
||||
if loadPlayer.currentVideo != nil && MP_NetWorkManager.shared.netWorkStatu == .reachable {
|
||||
previousEvent()
|
||||
return .success
|
||||
}else {
|
||||
@ -650,7 +649,7 @@ class MP_PlayerManager:NSObject{
|
||||
//下一首歌
|
||||
center!.nextTrackCommand.addTarget { [weak self] (event) in
|
||||
guard let self = self else { return .noActionableNowPlayingItem}
|
||||
if loadPlayer.currentVideo != nil {
|
||||
if loadPlayer.currentVideo != nil && MP_NetWorkManager.shared.netWorkStatu == .reachable {
|
||||
nextEvent()
|
||||
return .success
|
||||
}else {
|
||||
@ -660,7 +659,9 @@ class MP_PlayerManager:NSObject{
|
||||
//拖拽进度
|
||||
center?.changePlaybackPositionCommand.addTarget(handler: { [weak self] event in
|
||||
guard let self = self else { return .noActionableNowPlayingItem}
|
||||
|
||||
guard MP_NetWorkManager.shared.netWorkStatu == .reachable else {
|
||||
return .noActionableNowPlayingItem
|
||||
}
|
||||
guard let positionEvent = event as? MPChangePlaybackPositionCommandEvent else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
@ -17,18 +17,32 @@ class MPPositive_BrowseLoadViewModel: NSObject {
|
||||
MP_NetWorkManager.shared.browseRequestStateBlock = {
|
||||
[weak self] (lists,isCompleted) in
|
||||
guard let self = self else {return}
|
||||
//配置预览模块数据
|
||||
browseModuleLists.append(contentsOf: lists)
|
||||
//移除为空的数据
|
||||
browseModuleLists = browseModuleLists.filter{($0.items.count != 0)}
|
||||
if isCompleted == true {
|
||||
//将视频移动到最后
|
||||
if let index = browseModuleLists.firstIndex(where: {$0.items.first?.browseItem.pageType == "MUSIC_VIDEO_TYPE_OMV"}) {
|
||||
let removedElement = browseModuleLists.remove(at: index)
|
||||
browseModuleLists.append(removedElement)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
//配置预览模块数据
|
||||
browseModuleLists.append(contentsOf: lists)
|
||||
//移除为空的数据
|
||||
browseModuleLists = browseModuleLists.filter{($0.items.count != 0)}
|
||||
//通知首页刷新UI
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_browses_reload)
|
||||
}
|
||||
if isCompleted == true {
|
||||
//将视频移动到最后
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
|
||||
[weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
// 查找目标元素的索引
|
||||
if let index = browseModuleLists.firstIndex(where: {$0.items.first?.browseItem.pageType == "MUSIC_VIDEO_TYPE_OMV"}) {
|
||||
// 移除目标元素
|
||||
let removedElement = browseModuleLists.remove(at: index)
|
||||
// 将目标元素添加到数组的最后
|
||||
browseModuleLists.append(removedElement)
|
||||
}
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_browses_reload)
|
||||
})
|
||||
//加载完毕后
|
||||
MP_AnalyticsManager.shared.home_b_module_showsucces_actionAction()
|
||||
}
|
||||
|
||||
@ -43,12 +43,17 @@ class MPPositive_HomeViewController: MPPositive_BaseViewController{
|
||||
// //获取首页
|
||||
// MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists()
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(reloadAction(_ :)), notificationName: .positive_browses_reload)
|
||||
MP_HUD.loading()
|
||||
if MPPositive_BrowseLoadViewModel.shared.browseModuleLists.count == 0 {
|
||||
MP_HUD.loading()
|
||||
}
|
||||
errorBlock = {
|
||||
[weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
guard MPPositive_BrowseLoadViewModel.shared.browseModuleLists.count == 0 else {
|
||||
return
|
||||
}
|
||||
//移除所有(除了navView)的子控件,并告知页面处理错误,提示用户重试
|
||||
view.subviews.forEach { item in
|
||||
if item != self.navView {
|
||||
@ -75,6 +80,14 @@ class MPPositive_HomeViewController: MPPositive_BaseViewController{
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
// 请求跟踪授权
|
||||
requestTrackingAuthorization { idfa in
|
||||
}
|
||||
}
|
||||
}
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ class MPPositive_MoreContentViewController: MPPositive_BaseViewController {
|
||||
switch self {
|
||||
case .Single:
|
||||
layout.itemSize = .init(width: 168*width, height: 134*width)
|
||||
layout.minimumInteritemSpacing = 5*width
|
||||
layout.minimumInteritemSpacing = 2*width
|
||||
layout.minimumLineSpacing = 32*width
|
||||
case .List:
|
||||
layout.itemSize = .init(width: 162*width, height: 210*width)
|
||||
|
||||
@ -349,6 +349,7 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
coverView.downloadButton.isUserInteractionEnabled = !(MP_PlayerManager.shared.loadPlayer.currentVideo?.isDlownd ?? false)
|
||||
coverView.collectionSongBtn.isSelected = MP_PlayerManager.shared.loadPlayer.currentVideo?.isCollection ?? false
|
||||
coverView.restoreDownloadProgress()
|
||||
switchPlayTypeBtnIcon(typeBtn)
|
||||
// activityIndicator.isHidden = true
|
||||
// activityIndicator.stopAnimating()
|
||||
}
|
||||
|
||||
@ -38,6 +38,14 @@ class MPSideA_HomeViewController: MPSideA_BaseViewController {
|
||||
self?.reload()
|
||||
}
|
||||
}
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
// 请求跟踪授权
|
||||
requestTrackingAuthorization { idfa in
|
||||
}
|
||||
}
|
||||
}
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user