Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meta data support #131

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ExampleApp/ExampleApp/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ -(BOOL) canBecomeFirstResponder

-(void) audioPlayerViewPlayFromHTTPSelected:(AudioPlayerView*)audioPlayerView
{
NSURL* url = [NSURL URLWithString:@"file:///Users/tum/Temp/airplane-cut.aac"];
NSURL* url = [NSURL URLWithString:@"http://streaming.swisstxt.ch/m/rsj/mp3_128"];

STKDataSource* dataSource = [STKAudioPlayer dataSourceFromURL:url];

Expand Down
1 change: 1 addition & 0 deletions ExampleApp/ExampleApp/AudioPlayerView.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
NSTimer* timer;
UILabel* label;
UILabel* statusLabel;
UILabel* metaDataLabel;
UISlider* slider;
UISwitch* enableEqSwitch;
UISwitch* repeatSwitch;
Expand Down
19 changes: 17 additions & 2 deletions ExampleApp/ExampleApp/AudioPlayerView.m
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,18 @@ - (id)initWithFrame:(CGRect)frame andAudioPlayer:(STKAudioPlayer*)audioPlayerIn
label.textAlignment = NSTextAlignmentCenter;

statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, slider.frame.origin.y + slider.frame.size.height + label.frame.size.height + 8, frame.size.width, 50)];

statusLabel.textAlignment = NSTextAlignmentCenter;


metaDataLabel = [[UILabel alloc] initWithFrame:
CGRectMake(
0,
statusLabel.frame.origin.y - 20,
frame.size.width,
15
)];
metaDataLabel.font = [UIFont systemFontOfSize:12];
metaDataLabel.textAlignment = NSTextAlignmentCenter;

meter = [[UIView alloc] initWithFrame:CGRectMake(0, 450, 0, 20)];

meter.backgroundColor = [UIColor greenColor];
Expand All @@ -127,6 +136,7 @@ - (id)initWithFrame:(CGRect)frame andAudioPlayer:(STKAudioPlayer*)audioPlayerIn
[self addSubview:repeatSwitch];
[self addSubview:label];
[self addSubview:statusLabel];
[self addSubview:metaDataLabel];
[self addSubview:stopButton];
[self addSubview:meter];
[self addSubview:muteButton];
Expand Down Expand Up @@ -353,4 +363,9 @@ -(void) audioPlayer:(STKAudioPlayer *)audioPlayer logInfo:(NSString *)line
NSLog(@"%@", line);
}

-(void) audioPlayer:(STKAudioPlayer *)audioPlayer didUpdateMetaData:(NSDictionary *)metaData
{
metaDataLabel.text = metaData[@"StreamTitle"];
}

@end
14 changes: 13 additions & 1 deletion ExampleAppMac/ExampleAppMac/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ @interface AppDelegate()
{
NSView* meter;
NSSlider* slider;
NSTextField* metaDataTextField;
STKAudioPlayer* audioPlayer;
}
@end
Expand All @@ -36,9 +37,14 @@ -(void) applicationDidFinishLaunching:(NSNotification *)aNotification
[meter setWantsLayer:YES];
meter.layer.backgroundColor = [NSColor greenColor].CGColor;

metaDataTextField = [[NSTextField alloc] initWithFrame:CGRectMake(10, 270, frame.size.width - 20, 80)];
metaDataTextField.alignment = NSCenterTextAlignment;
metaDataTextField.stringValue = @"no meta data";

[[self.window contentView] addSubview:slider];
[[self.window contentView] addSubview:playFromHTTPButton];
[[self.window contentView] addSubview:meter];
[[self.window contentView] addSubview:metaDataTextField];

audioPlayer = [[STKAudioPlayer alloc] initWithOptions:(STKAudioPlayerOptions){ .enableVolumeMixer = NO, .equalizerBandFrequencies = {50, 100, 200, 400, 800, 1600, 2600, 16000} } ];
audioPlayer.delegate = self;
Expand All @@ -58,7 +64,8 @@ -(void) test

-(void) playFromHTTP
{
[audioPlayer play:@"http://www.abstractpath.com/files/audiosamples/sample.mp3"];
// Swiss Jazz stream
[audioPlayer play:@"http://streaming.swisstxt.ch/m/rsj/mp3_128"];
}

-(void) tick:(NSTimer*)timer
Expand Down Expand Up @@ -126,4 +133,9 @@ -(void) audioPlayer:(STKAudioPlayer*)audioPlayer unexpectedError:(STKAudioPlayer
{
}

-(void) audioPlayer:(STKAudioPlayer *)audioPlayer didUpdateMetaData:(NSDictionary *)metaData
{
metaDataTextField.stringValue = [metaData description];
}

@end
3 changes: 3 additions & 0 deletions StreamingKit/StreamingKit/STKAudioPlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ typedef void(^STKFrameFilter)(UInt32 channelsPerFrame, UInt32 bytesPerFrame, UIn
/// Raised when items queued items are cleared (usually because of a call to play, setDataSource or stop)
-(void) audioPlayer:(STKAudioPlayer*)audioPlayer didCancelQueuedItems:(NSArray*)queuedItems;

// Raised when new meta data arrives
-(void)audioPlayer:(STKAudioPlayer *)audioPlayer didUpdateMetaData:(NSDictionary *)metaData;

@end

@interface STKAudioPlayer : NSObject<STKDataSourceDelegate>
Expand Down
7 changes: 7 additions & 0 deletions StreamingKit/StreamingKit/STKAudioPlayer.m
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,13 @@ -(void) dataSourceEof:(STKDataSource*)dataSourceIn
[self processRunloop];
}

-(void) dataSource:(STKDataSource *)dataSource didUpdateMetaData:(NSDictionary *)metaDataDictionary
{
if ([self.delegate respondsToSelector:@selector(audioPlayer:didUpdateMetaData:)]) {
[self.delegate audioPlayer:self didUpdateMetaData:metaDataDictionary];
}
}

-(void) pause
{
pthread_mutex_lock(&playerMutex);
Expand Down
3 changes: 3 additions & 0 deletions StreamingKit/StreamingKit/STKDataSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
-(void) dataSourceDataAvailable:(STKDataSource*)dataSource;
-(void) dataSourceErrorOccured:(STKDataSource*)dataSource;
-(void) dataSourceEof:(STKDataSource*)dataSource;

-(void) dataSource:(STKDataSource*)dataSource didUpdateMetaData:(NSDictionary *)metaDataDictionary;

@end

@interface STKDataSource : NSObject
Expand Down
5 changes: 5 additions & 0 deletions StreamingKit/StreamingKit/STKDataSourceWrapper.m
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,9 @@ -(void) dataSourceEof:(STKDataSource*)dataSource
[self.delegate dataSourceEof:self];
}

-(void) dataSource:(STKDataSource *)dataSource didUpdateMetaData:(NSDictionary *)metaDataDictionary
{
[self.delegate dataSource:self didUpdateMetaData:metaDataDictionary];
}

@end
156 changes: 156 additions & 0 deletions StreamingKit/StreamingKit/STKHTTPDataSource.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ @interface STKHTTPDataSource()
NSDictionary* httpHeaders;
AudioFileTypeID audioFileTypeHint;
NSDictionary* requestHeaders;

// Meta data
BOOL metaDataPresent;
unsigned int metaDataInterval; // how many data bytes between meta data
unsigned int metaDataBytesRemaining; // how many bytes of metadata remain to be read
unsigned int dataBytesRead; // how many bytes of data have been read
BOOL foundIcyStart;
BOOL foundIcyEnd;
NSMutableString *metaDataString; // meta data string
}
-(void) open;

Expand Down Expand Up @@ -90,6 +99,8 @@ -(id) initWithAsyncURLProvider:(STKAsyncURLProvider)asyncUrlProviderIn
self->asyncUrlProvider = [asyncUrlProviderIn copy];

audioFileTypeHint = [STKLocalFileDataSource audioFileTypeHintFromFileExtension:self->currentUrl.pathExtension];

metaDataString = [NSMutableString new];
}

return self;
Expand Down Expand Up @@ -267,6 +278,10 @@ -(int) readIntoBuffer:(UInt8*)buffer withSize:(int)size
return read;
}

// method will move audio bytes to the beginning of the buffer,
// and return their number
read = [self checkForMetaDataInfoWithBuffer:buffer andLength:read];

relativePosition += read;

return read;
Expand Down Expand Up @@ -313,6 +328,8 @@ -(void) openForSeek:(BOOL)forSeek
CFHTTPMessageSetHeaderFieldValue(message, (__bridge CFStringRef)key, (__bridge CFStringRef)value);
}

CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Icy-MetaData"), CFSTR("1"));

stream = CFReadStreamCreateForHTTPRequest(NULL, message);

if (stream == nil)
Expand Down Expand Up @@ -394,4 +411,143 @@ -(NSString*) description
return [NSString stringWithFormat:@"HTTP data source with file length: %lld and position: %lld", self.length, self.position];
}

#pragma mark - Meta data

// This code was mostly taken from the link below
// https://code.google.com/p/audiostreamer-meta/

// Returns new length: the number of bytes from buffer that contain audio data.
// Other bytes are meta data bytes and this method "consumes" them.
-(int) checkForMetaDataInfoWithBuffer:(UInt8 *)buffer andLength:(int)length
{
CFHTTPMessageRef response = (CFHTTPMessageRef)CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader);

if (foundIcyStart == NO && metaDataPresent == NO) {
// check if this is a ICY 200 OK response
NSString *icyCheck = [[NSString alloc] initWithBytes:buffer length:10 encoding:NSUTF8StringEncoding];
if (icyCheck != nil && [icyCheck caseInsensitiveCompare:@"ICY 200 OK"] == NSOrderedSame) {
foundIcyStart = YES;
} else {
NSString *metaInt = (__bridge NSString *) CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("Icy-Metaint"));

if (metaInt) {
metaDataPresent = YES;
metaDataInterval = [metaInt intValue];
}
}
}

int streamStart = 0;

if (foundIcyStart == YES && foundIcyEnd == NO) {
char c[4] = {};

for (int lineStart = 0; streamStart + 3 < length; ++streamStart) {

memcpy(c, buffer + streamStart, 4);

if (c[0] == '\r' && c[1] == '\n') {
NSString *fullString = [[NSString alloc] initWithBytes:buffer length:streamStart encoding:NSUTF8StringEncoding];

NSString *line = [fullString substringWithRange:NSMakeRange(lineStart, streamStart - lineStart)];

NSArray *lineItems = [line componentsSeparatedByString:@":"];
if (lineItems.count > 1) {
if ([lineItems[0] caseInsensitiveCompare:@"icy-metaint"] == NSOrderedSame) {
metaDataInterval = [lineItems[1] intValue];
} else if ([lineItems[0] caseInsensitiveCompare:@"content-type"] == NSOrderedSame) {
AudioFileTypeID idFromMime = [STKHTTPDataSource audioFileTypeHintFromMimeType:lineItems[1]];
if (idFromMime != 0) {
audioFileTypeHint = idFromMime;
}
}
}

// this is the end of a line, the new line starts in 2
lineStart = streamStart + 2;

if (c[2] == '\r' && c[3] == '\n') {
foundIcyEnd = YES;
metaDataPresent = YES;
streamStart += 4; // skip double new line
break;
}
}
}
}

if (metaDataPresent == YES) {
int audioDataByteCount = 0;

for (int i = streamStart; i < length; ++i) {
// is this a metadata byte?
if (metaDataBytesRemaining > 0) {

[metaDataString appendFormat:@"%c", buffer[i]];

if (--metaDataBytesRemaining == 0) {
dataBytesRead = 0;

NSDictionary *metaDataDictionary = [self dictionaryFromMetaData:metaDataString];
[self.delegate dataSource:self didUpdateMetaData:metaDataDictionary];
}

continue;
}

// is this the interval byte?
if (metaDataInterval > 0 && dataBytesRead == metaDataInterval) {

metaDataBytesRemaining = buffer[i] * 16;

metaDataString.string = @"";

if (metaDataBytesRemaining == 0) {
dataBytesRead = 0;
}

continue;
}

// this is a data byte
++dataBytesRead;

// overwrite beginning of the buffer with the real audio data
// we don't need those bytes any more, since we already examined them
buffer[audioDataByteCount++] = buffer[i];
}

return audioDataByteCount;

} else if (foundIcyStart == YES) { // still parsing icy response

return 0;

} else { // no meta data in stream

return length;

}
}

-(NSDictionary *) dictionaryFromMetaData:(NSString *)metaData
{
NSArray *components = [metaData componentsSeparatedByString:@";"];

NSMutableDictionary *dictionary = [NSMutableDictionary new];

for (NSString *entry in components) {
NSInteger equalitySignPosition = [entry rangeOfString:@"="].location;
if (equalitySignPosition != NSNotFound) {
NSString *key = [entry substringToIndex:equalitySignPosition];
NSString *value = [entry substringFromIndex:equalitySignPosition + 1];
NSString *valueWithoutQuotes = [value substringWithRange:NSMakeRange(1, value.length - 2)];

dictionary[key] = valueWithoutQuotes;
}
}

return dictionary;
}

@end