HTTP Reader: Replaced implementation with libCURL
New implementation largely based on the vfs_curl module from DeaDBeeF. Signed-off-by: Christopher Snowhill <kode54@gmail.com>CQTexperiment
parent
e13f83609e
commit
69506cd1d7
|
@ -10,29 +10,64 @@
|
|||
|
||||
#import "Plugin.h"
|
||||
|
||||
@class HTTPConnection;
|
||||
#include <curl/curl.h>
|
||||
|
||||
@interface HTTPSource : NSObject <CogSource, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate> {
|
||||
NSOperationQueue *queue;
|
||||
#define BUFFER_SIZE 0x10000
|
||||
#define BUFFER_MASK 0xffff
|
||||
|
||||
#define MAX_METADATA 1024
|
||||
|
||||
#define TIMEOUT 10 // in seconds
|
||||
|
||||
enum {
|
||||
STATUS_UNSTARTED = 0,
|
||||
STATUS_INITIAL = 1,
|
||||
STATUS_READING = 2,
|
||||
STATUS_FINISHED = 3,
|
||||
STATUS_ABORTED = 4,
|
||||
STATUS_SEEK = 5,
|
||||
};
|
||||
|
||||
@interface HTTPSource : NSObject <CogSource> {
|
||||
NSURL *URL;
|
||||
NSURLSession *session;
|
||||
NSURLSessionDataTask *task;
|
||||
|
||||
Boolean didReceiveResponse;
|
||||
Boolean didReceiveRandomData;
|
||||
Boolean didComplete;
|
||||
int64_t pos; // position in stream; use "& BUFFER_MASK" to make it index into ringbuffer
|
||||
int64_t length;
|
||||
int32_t remaining; // remaining bytes in buffer read from stream
|
||||
int64_t skipbytes;
|
||||
uint8_t buffer[BUFFER_SIZE];
|
||||
|
||||
Boolean redirected;
|
||||
NSMutableArray *redirectURLs;
|
||||
NSLock *mutex;
|
||||
|
||||
NSMutableData *bufferedData;
|
||||
uint8_t nheaderpackets;
|
||||
NSString *content_type;
|
||||
CURL *curl;
|
||||
struct timeval last_read_time;
|
||||
uint8_t status;
|
||||
int icy_metaint;
|
||||
int wait_meta;
|
||||
|
||||
long _bytesBuffered;
|
||||
long _byteCount;
|
||||
BOOL taskSuspended;
|
||||
char metadata[MAX_METADATA];
|
||||
size_t metadata_size; // size of metadata in stream
|
||||
size_t metadata_have_size; // amount which is already in metadata buffer
|
||||
|
||||
NSString *_mimeType;
|
||||
char http_err[CURL_ERROR_SIZE];
|
||||
|
||||
BOOL need_abort;
|
||||
|
||||
NSString *album;
|
||||
NSString *artist;
|
||||
NSString *title;
|
||||
NSString *genre;
|
||||
|
||||
// flags (bitfields to save some space)
|
||||
unsigned seektoend : 1; // indicates that next tell must return length
|
||||
unsigned gotheader : 1; // tells that all headers (including ICY) were processed (to start reading body)
|
||||
unsigned icyheader : 1; // tells that we're currently reading ICY headers
|
||||
unsigned gotsomeheader : 1; // tells that we got some headers before body started
|
||||
unsigned gotmetadata : 1; // got some metadata
|
||||
}
|
||||
|
||||
- (BOOL)hasMetadata;
|
||||
- (NSDictionary *)metadata;
|
||||
@end
|
||||
|
|
|
@ -14,269 +14,723 @@
|
|||
#import <stdlib.h>
|
||||
#import <string.h>
|
||||
|
||||
#define BUFFER_SIZE 262144
|
||||
|
||||
@implementation HTTPSource
|
||||
|
||||
- (NSURLSession *)createSession {
|
||||
queue = [[NSOperationQueue alloc] init];
|
||||
static size_t http_curl_write_wrapper(HTTPSource *fp, void *ptr, size_t size) {
|
||||
size_t avail = size;
|
||||
while(avail > 0) {
|
||||
[fp->mutex lock];
|
||||
if(fp->status == STATUS_SEEK) {
|
||||
DLog(@"curl seek request, aborting current request");
|
||||
[fp->mutex unlock];
|
||||
return 0;
|
||||
}
|
||||
if(fp->need_abort) {
|
||||
fp->status = STATUS_ABORTED;
|
||||
DLog(@"curl STATUS_ABORTED in the middle of packet");
|
||||
[fp->mutex unlock];
|
||||
break;
|
||||
}
|
||||
int sz = BUFFER_SIZE / 2 - fp->remaining; // number of bytes free in buffer
|
||||
// don't allow to fill more than half -- used for seeking backwards
|
||||
|
||||
NSURLSession *session = nil;
|
||||
session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
|
||||
delegate:self
|
||||
delegateQueue:queue];
|
||||
return session;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveData:(NSData *)data {
|
||||
long bytesBuffered = 0;
|
||||
if(!task) return;
|
||||
if(didReceiveRandomData) {
|
||||
// Parse ICY header here?
|
||||
// XXX
|
||||
didReceiveRandomData = NO;
|
||||
|
||||
const char *header = "ICY 200 OK\r\n";
|
||||
size_t length = [data length];
|
||||
if(length >= strlen(header)) {
|
||||
const char *dataBytes = (const char *)[data bytes];
|
||||
const char *dataStart = dataBytes;
|
||||
if(memcmp(dataBytes, header, strlen(header)) == 0) {
|
||||
const char *dataEnd = dataBytes + length;
|
||||
Boolean endFound = NO;
|
||||
while(dataBytes + 4 <= dataEnd) {
|
||||
if(memcmp(dataBytes, "\r\n\r\n", 4) == 0) {
|
||||
endFound = YES;
|
||||
break;
|
||||
}
|
||||
dataBytes++;
|
||||
}
|
||||
if(!endFound) {
|
||||
@synchronized(task) {
|
||||
didComplete = YES;
|
||||
[task cancel];
|
||||
task = nil;
|
||||
return;
|
||||
}
|
||||
}
|
||||
dataEnd = dataBytes + 4;
|
||||
NSUInteger dataLeft = length - (dataEnd - dataStart);
|
||||
dataBytes = dataStart;
|
||||
dataBytes += strlen("ICY 200 OK\r\n");
|
||||
char headerBuffer[80 * 1024 + 1];
|
||||
while(dataBytes < dataEnd - 2) {
|
||||
const char *string = dataBytes;
|
||||
while(dataBytes < dataEnd - 2) {
|
||||
if(memcmp(dataBytes, "\r\n", 2) == 0) break;
|
||||
dataBytes++;
|
||||
}
|
||||
if(dataBytes - string > 80 * 1024)
|
||||
dataBytes = string + 80 * 1024;
|
||||
strncpy(headerBuffer, string, dataBytes - string);
|
||||
headerBuffer[dataBytes - string] = '\0';
|
||||
|
||||
char *colon = strchr(headerBuffer, ':');
|
||||
if(colon) {
|
||||
*colon = '\0';
|
||||
colon++;
|
||||
}
|
||||
|
||||
if(strcasecmp(headerBuffer, "content-type") == 0) {
|
||||
_mimeType = [NSString stringWithUTF8String:colon];
|
||||
}
|
||||
|
||||
dataBytes += 2;
|
||||
}
|
||||
|
||||
data = [NSData dataWithBytes:dataEnd length:dataLeft];
|
||||
|
||||
didReceiveResponse = YES;
|
||||
if(sz > 5000) { // wait until there are at least 5k bytes free
|
||||
size_t cp = MIN(avail, sz);
|
||||
int writepos = (fp->pos + fp->remaining) & BUFFER_MASK;
|
||||
// copy 1st portion (before end of buffer
|
||||
size_t part1 = BUFFER_SIZE - writepos;
|
||||
// may not be more than total
|
||||
part1 = MIN(part1, cp);
|
||||
memcpy(fp->buffer + writepos, ptr, part1);
|
||||
ptr += part1;
|
||||
avail -= part1;
|
||||
fp->remaining += part1;
|
||||
cp -= part1;
|
||||
if(cp > 0) {
|
||||
memcpy(fp->buffer, ptr, cp);
|
||||
ptr += cp;
|
||||
avail -= cp;
|
||||
fp->remaining += cp;
|
||||
}
|
||||
}
|
||||
[fp->mutex unlock];
|
||||
usleep(3000);
|
||||
}
|
||||
@synchronized(bufferedData) {
|
||||
[bufferedData appendData:data];
|
||||
_bytesBuffered += [data length];
|
||||
bytesBuffered = _bytesBuffered;
|
||||
}
|
||||
if(bytesBuffered >= BUFFER_SIZE) {
|
||||
[task suspend];
|
||||
taskSuspended = YES;
|
||||
}
|
||||
return size - avail;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveResponse:(NSURLResponse *)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
|
||||
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if(statusCode != 200) {
|
||||
if([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
completionHandler(NSURLSessionResponseCancel);
|
||||
@synchronized(task) {
|
||||
task = nil;
|
||||
static int http_parse_shoutcast_meta(HTTPSource *fp, const char *meta, size_t size) {
|
||||
// DLog (@"reading %d bytes of metadata\n", size);
|
||||
DLog(@"%s", meta);
|
||||
const char *e = meta + size;
|
||||
const char strtitle[] = "StreamTitle='";
|
||||
char title[256] = "";
|
||||
while(meta < e) {
|
||||
if(!memcmp(meta, strtitle, sizeof(strtitle) - 1)) {
|
||||
meta += sizeof(strtitle) - 1;
|
||||
const char *substr_end = meta;
|
||||
while(substr_end < e - 1 && (*substr_end != '\'' || *(substr_end + 1) != ';')) {
|
||||
substr_end++;
|
||||
}
|
||||
return;
|
||||
if(substr_end >= e) {
|
||||
return -1; // end of string not found
|
||||
}
|
||||
size_t s = substr_end - meta;
|
||||
s = MIN(sizeof(title) - 1, s);
|
||||
memcpy(title, meta, s);
|
||||
title[s] = 0;
|
||||
DLog(@"got stream title: %s\n", title);
|
||||
{
|
||||
char *tit = strstr(title, " - ");
|
||||
if(tit) {
|
||||
*tit = 0;
|
||||
tit += 3;
|
||||
|
||||
const char *orig_title = [fp->title UTF8String];
|
||||
const char *orig_artist = [fp->artist UTF8String];
|
||||
|
||||
if(!orig_title || strcasecmp(orig_title, tit)) {
|
||||
fp->title = [NSString stringWithUTF8String:tit];
|
||||
fp->gotmetadata = 1;
|
||||
}
|
||||
if(!orig_artist || strcasecmp(orig_artist, title)) {
|
||||
fp->artist = [NSString stringWithUTF8String:title];
|
||||
fp->gotmetadata = 1;
|
||||
}
|
||||
} else {
|
||||
const char *orig_title = [fp->title UTF8String];
|
||||
if(!orig_title || strcasecmp(orig_title, title)) {
|
||||
fp->artist = @"";
|
||||
fp->title = [NSString stringWithUTF8String:title];
|
||||
fp->gotmetadata = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
while(meta < e && *meta != ';') {
|
||||
meta++;
|
||||
}
|
||||
if(meta < e) {
|
||||
meta++;
|
||||
}
|
||||
}
|
||||
_mimeType = [response MIMEType];
|
||||
if([_mimeType isEqualToString:@"application/octet-stream"] ||
|
||||
[_mimeType isEqualToString:@"text/plain"])
|
||||
didReceiveRandomData = YES;
|
||||
else
|
||||
didReceiveResponse = YES;
|
||||
|
||||
completionHandler(NSURLSessionResponseAllow);
|
||||
return -1;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
newRequest:(NSURLRequest *)request
|
||||
completionHandler:(void (^)(NSURLRequest *))completionHandler {
|
||||
NSURL *url = [request URL];
|
||||
if([redirectURLs containsObject:url]) {
|
||||
completionHandler(nil);
|
||||
@synchronized(self->task) {
|
||||
self->task = nil;
|
||||
static const uint8_t *parse_header(const uint8_t *p, const uint8_t *e, uint8_t *key, int keysize, uint8_t *value, int valuesize) {
|
||||
size_t sz; // will hold length of extracted string
|
||||
const uint8_t *v; // pointer to current character
|
||||
keysize--;
|
||||
valuesize--;
|
||||
*key = 0;
|
||||
*value = 0;
|
||||
v = p;
|
||||
// find :
|
||||
while(v < e && *v != 0x0d && *v != 0x0a && *v != ':') {
|
||||
v++;
|
||||
}
|
||||
if(*v != ':') {
|
||||
// skip linebreaks
|
||||
while(v < e && (*v == 0x0d || *v == 0x0a)) {
|
||||
v++;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
// copy key
|
||||
sz = v - p;
|
||||
sz = MIN(keysize, sz);
|
||||
memcpy(key, p, sz);
|
||||
key[sz] = 0;
|
||||
|
||||
// skip whitespace
|
||||
v++;
|
||||
while(v < e && (*v == 0x20 || *v == 0x08)) {
|
||||
v++;
|
||||
}
|
||||
if(*v == 0x0d || *v == 0x0a) {
|
||||
// skip linebreaks
|
||||
while(v < e && (*v == 0x0d || *v == 0x0a)) {
|
||||
v++;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
p = v;
|
||||
|
||||
// find linebreak
|
||||
while(v < e && *v != 0x0d && *v != 0x0a) {
|
||||
v++;
|
||||
}
|
||||
|
||||
// copy value
|
||||
sz = v - p;
|
||||
sz = MIN(valuesize, sz);
|
||||
memcpy(value, p, sz);
|
||||
value[sz] = 0;
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
static size_t http_content_header_handler_int(void *ptr, size_t size, void *stream, int *end_of_headers) {
|
||||
// DLog(@"http_content_header_handler\n");
|
||||
assert(stream);
|
||||
HTTPSource *fp = (__bridge HTTPSource *)stream;
|
||||
const uint8_t *p = ptr;
|
||||
const uint8_t *end = p + size;
|
||||
uint8_t key[256];
|
||||
uint8_t value[256];
|
||||
|
||||
if(fp->length == 0) {
|
||||
fp->length = -1;
|
||||
}
|
||||
|
||||
while(p < end) {
|
||||
if(p <= end - 4) {
|
||||
if(!memcmp(p, "\r\n\r\n", 4)) {
|
||||
p += 4;
|
||||
*end_of_headers = 1;
|
||||
return p - (uint8_t *)ptr;
|
||||
}
|
||||
}
|
||||
// skip linebreaks
|
||||
while(p < end && (*p == 0x0d || *p == 0x0a)) {
|
||||
p++;
|
||||
}
|
||||
p = parse_header(p, end, key, sizeof(key), value, sizeof(value));
|
||||
DLog(@"%skey=%s value=%s\n", fp->icyheader ? "[icy] " : "", key, value);
|
||||
if(!strcasecmp((char *)key, "Content-Type")) {
|
||||
fp->content_type = [NSString stringWithUTF8String:(const char *)value];
|
||||
} else if(!strcasecmp((char *)key, "Content-Length")) {
|
||||
char *end;
|
||||
fp->length = strtol((const char *)value, &end, 10);
|
||||
} else if(!strcasecmp((char *)key, "icy-name")) {
|
||||
fp->title = [NSString stringWithUTF8String:(const char *)value];
|
||||
fp->gotmetadata = 1;
|
||||
} else if(!strcasecmp((char *)key, "icy-genre")) {
|
||||
fp->genre = [NSString stringWithUTF8String:(const char *)value];
|
||||
fp->gotmetadata = 1;
|
||||
} else if(!strcasecmp((char *)key, "icy-metaint")) {
|
||||
// printf ("icy-metaint: %d\n", atoi (value));
|
||||
char *end;
|
||||
fp->icy_metaint = (int)strtoul((const char *)value, &end, 10);
|
||||
fp->wait_meta = fp->icy_metaint;
|
||||
} else if(!strcasecmp((char *)key, "icy-url")) {
|
||||
fp->album = [NSString stringWithUTF8String:(const char *)value];
|
||||
fp->gotmetadata = 1;
|
||||
}
|
||||
|
||||
// for icy streams, reset length
|
||||
if(!strncasecmp((char *)key, "icy-", 4)) {
|
||||
fp->length = -1;
|
||||
}
|
||||
}
|
||||
if(!fp->icyheader) {
|
||||
fp->gotsomeheader = 1;
|
||||
}
|
||||
return p - (uint8_t *)ptr;
|
||||
}
|
||||
|
||||
static size_t handle_icy_headers(size_t avail, HTTPSource *fp, char *ptr) {
|
||||
size_t size = avail;
|
||||
|
||||
// check if that's ICY
|
||||
if(!fp->icyheader && avail >= 10 && !memcmp(ptr, "ICY 200 OK", 10)) {
|
||||
DLog(@"icy headers in the stream");
|
||||
ptr += 10;
|
||||
avail -= 10;
|
||||
|
||||
fp->icyheader = 1;
|
||||
|
||||
// check for ternmination marker
|
||||
if(avail >= 4 && !memcmp(ptr, "\r\n\r\n", 4)) {
|
||||
avail -= 4;
|
||||
ptr += 4;
|
||||
fp->gotheader = 1;
|
||||
|
||||
return size - avail;
|
||||
}
|
||||
|
||||
// skip remaining linebreaks
|
||||
while(avail > 0 && (*ptr == '\r' || *ptr == '\n')) {
|
||||
avail--;
|
||||
ptr++;
|
||||
}
|
||||
}
|
||||
if(fp->icyheader) {
|
||||
if(fp->nheaderpackets > 10) {
|
||||
DLog(@"curl: warning: seems like stream has unterminated ICY headers");
|
||||
fp->icy_metaint = 0;
|
||||
fp->wait_meta = 0;
|
||||
fp->gotheader = 1;
|
||||
} else if(avail) {
|
||||
fp->nheaderpackets++;
|
||||
|
||||
int end = 0;
|
||||
size_t consumed = http_content_header_handler_int(ptr, avail, (__bridge void *)fp, &end);
|
||||
avail -= consumed;
|
||||
ptr += consumed;
|
||||
fp->gotheader = end || (avail != 0);
|
||||
}
|
||||
} else {
|
||||
[redirectURLs addObject:url];
|
||||
redirected = YES;
|
||||
didReceiveResponse = NO;
|
||||
didComplete = NO;
|
||||
@synchronized(bufferedData) {
|
||||
[bufferedData setLength:0];
|
||||
_bytesBuffered = 0;
|
||||
fp->gotheader = 1;
|
||||
}
|
||||
if(!avail) {
|
||||
return size;
|
||||
}
|
||||
|
||||
return size - avail;
|
||||
}
|
||||
|
||||
static size_t _handle_icy_metadata(size_t avail, HTTPSource *fp, char *ptr, int *error) {
|
||||
size_t size = avail;
|
||||
while(fp->icy_metaint > 0) {
|
||||
if(fp->metadata_size > 0) {
|
||||
if(fp->metadata_size > fp->metadata_have_size) {
|
||||
DLog(@"metadata fetch mode, avail: %zu, metadata_size: %zu, metadata_have_size: %zu)", avail, fp->metadata_size, fp->metadata_have_size);
|
||||
size_t sz = (fp->metadata_size - fp->metadata_have_size);
|
||||
sz = MIN(sz, avail);
|
||||
size_t space = MAX_METADATA - fp->metadata_have_size;
|
||||
size_t copysize = MIN(space, sz);
|
||||
if(copysize > 0) {
|
||||
DLog(@"fetching %zu bytes of metadata (out of %zu)", sz, fp->metadata_size);
|
||||
memcpy(fp->metadata + fp->metadata_have_size, ptr, copysize);
|
||||
}
|
||||
avail -= sz;
|
||||
ptr += sz;
|
||||
fp->metadata_have_size += sz;
|
||||
}
|
||||
if(fp->metadata_size == fp->metadata_have_size) {
|
||||
size_t sz = fp->metadata_size;
|
||||
fp->metadata_size = fp->metadata_have_size = 0;
|
||||
if(http_parse_shoutcast_meta(fp, fp->metadata, sz) < 0) {
|
||||
fp->metadata_size = 0;
|
||||
fp->metadata_have_size = 0;
|
||||
fp->wait_meta = 0;
|
||||
fp->icy_metaint = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(fp->wait_meta < avail) {
|
||||
// read bytes remaining until metadata block
|
||||
size_t res1 = http_curl_write_wrapper(fp, ptr, fp->wait_meta);
|
||||
if(res1 != fp->wait_meta) {
|
||||
*error = 1;
|
||||
return 0;
|
||||
}
|
||||
avail -= res1;
|
||||
ptr += res1;
|
||||
uint32_t sz = (uint32_t)(*((uint8_t *)ptr)) * 16;
|
||||
if(sz > MAX_METADATA) {
|
||||
DLog(@"metadata size %d is too large\n", sz);
|
||||
ptr += sz;
|
||||
fp->metadata_size = 0;
|
||||
fp->metadata_have_size = 0;
|
||||
fp->wait_meta = 0;
|
||||
fp->icy_metaint = 0;
|
||||
break;
|
||||
}
|
||||
// assert (sz < MAX_METADATA);
|
||||
ptr++;
|
||||
fp->metadata_size = sz;
|
||||
fp->metadata_have_size = 0;
|
||||
fp->wait_meta = fp->icy_metaint;
|
||||
avail--;
|
||||
if(sz != 0) {
|
||||
DLog(@"found metadata block at pos %lld, size: %d (avail=%zu)\n", fp->pos, sz, avail);
|
||||
}
|
||||
}
|
||||
if((!fp->metadata_size || !avail) && fp->wait_meta >= avail) {
|
||||
break;
|
||||
}
|
||||
if(avail < 0) {
|
||||
DLog(@"curl: something bad happened in metadata parser. can't continue streaming.\n");
|
||||
*error = 1;
|
||||
return 0;
|
||||
}
|
||||
completionHandler(request);
|
||||
}
|
||||
return size - avail;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
didBecomeInvalidWithError:(NSError *)error {
|
||||
@synchronized(task) {
|
||||
task = nil;
|
||||
static size_t http_curl_write(void *_ptr, size_t size, size_t nmemb, void *stream) {
|
||||
char *ptr = _ptr;
|
||||
size_t avail = size * nmemb;
|
||||
HTTPSource *fp = (__bridge HTTPSource *)stream;
|
||||
|
||||
// DLog(@"http_curl_write %d bytes, wait_meta=%d\n", size * nmemb, fp->wait_meta);
|
||||
gettimeofday(&fp->last_read_time, NULL);
|
||||
if(fp->need_abort) {
|
||||
fp->status = STATUS_ABORTED;
|
||||
DLog(@"curl STATUS_ABORTED at start of packet");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// process the in-stream headers, if present
|
||||
if(!fp->gotheader) {
|
||||
size_t consumed = handle_icy_headers(avail, fp, ptr);
|
||||
avail -= consumed;
|
||||
ptr += consumed;
|
||||
if(!avail) {
|
||||
return nmemb * size;
|
||||
}
|
||||
}
|
||||
|
||||
[fp->mutex lock];
|
||||
if(fp->status == STATUS_INITIAL && fp->gotheader) {
|
||||
fp->status = STATUS_READING;
|
||||
}
|
||||
[fp->mutex unlock];
|
||||
|
||||
int error = 0;
|
||||
size_t consumed = _handle_icy_metadata(avail, fp, ptr, &error);
|
||||
if(error) {
|
||||
return 0;
|
||||
}
|
||||
avail -= consumed;
|
||||
ptr += consumed;
|
||||
|
||||
// the remaining bytes are the normal stream, without metadata or headers
|
||||
if(avail) {
|
||||
// DLog(@"http_curl_write_wrapper [2] %d\n", avail);
|
||||
size_t res = http_curl_write_wrapper(fp, ptr, avail);
|
||||
avail -= res;
|
||||
fp->wait_meta -= res;
|
||||
}
|
||||
return nmemb * size - avail;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
willCacheResponse:(NSCachedURLResponse *)proposedResponse
|
||||
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
|
||||
didComplete = YES;
|
||||
completionHandler(nil);
|
||||
static size_t http_content_header_handler(void *ptr, size_t size, size_t nmemb, void *stream) {
|
||||
int end = 0;
|
||||
return http_content_header_handler_int(ptr, size * nmemb, stream, &end);
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(NSError *)error {
|
||||
@synchronized(self->task) {
|
||||
self->task = nil;
|
||||
static int http_curl_control(void *stream, double dltotal, double dlnow, double ultotal, double ulnow) {
|
||||
HTTPSource *fp = (__bridge HTTPSource *)stream;
|
||||
|
||||
[fp->mutex lock];
|
||||
|
||||
struct timeval tm;
|
||||
gettimeofday(&tm, NULL);
|
||||
float sec = tm.tv_sec - fp->last_read_time.tv_sec;
|
||||
long response;
|
||||
curl_easy_getinfo(fp->curl, CURLINFO_RESPONSE_CODE, &response);
|
||||
// DLog ("http_curl_control: status = %d, response = %d, interval: %f seconds\n", fp ? fp->status : -1, (int)response, sec);
|
||||
if(fp->status == STATUS_READING && sec > TIMEOUT) {
|
||||
DLog(@"http_curl_control: timed out, restarting read");
|
||||
memcpy(&fp->last_read_time, &tm, sizeof(struct timeval));
|
||||
http_stream_reset(fp);
|
||||
fp->status = STATUS_SEEK;
|
||||
} else if(fp->status == STATUS_SEEK) {
|
||||
DLog(@"curl STATUS_SEEK in progress callback");
|
||||
[fp->mutex unlock];
|
||||
return -1;
|
||||
}
|
||||
if(fp->need_abort) {
|
||||
fp->status = STATUS_ABORTED;
|
||||
DLog(@"curl STATUS_ABORTED in progress callback");
|
||||
[fp->mutex unlock];
|
||||
return -1;
|
||||
}
|
||||
[fp->mutex unlock];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void http_stream_reset(HTTPSource *fp) {
|
||||
fp->gotheader = 0;
|
||||
fp->icyheader = 0;
|
||||
fp->gotsomeheader = 0;
|
||||
fp->remaining = 0;
|
||||
fp->metadata_size = 0;
|
||||
fp->metadata_have_size = 0;
|
||||
fp->skipbytes = 0;
|
||||
fp->nheaderpackets = 0;
|
||||
fp->icy_metaint = 0;
|
||||
fp->wait_meta = 0;
|
||||
}
|
||||
|
||||
- (void)threadEntry:(id)info {
|
||||
@autoreleasepool {
|
||||
CURL *curl;
|
||||
curl = curl_easy_init();
|
||||
length = -1;
|
||||
self->curl = curl;
|
||||
self->status = STATUS_INITIAL;
|
||||
|
||||
int status;
|
||||
|
||||
DLog(@"curl: started loading data %@", URL);
|
||||
for(;;) {
|
||||
struct curl_slist *headers = NULL;
|
||||
struct curl_slist *ok_aliases = curl_slist_append(NULL, "ICY 200 OK");
|
||||
|
||||
curl_easy_reset(curl);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, [[URL absoluteString] UTF8String]);
|
||||
NSString *ua = [NSString stringWithFormat:@"Cog/%@", [[[NSBundle mainBundle] infoDictionary] valueForKey:(__bridge NSString *)kCFBundleVersionKey]];
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, [ua UTF8String]);
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_curl_write);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (__bridge void *)self);
|
||||
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, http_err);
|
||||
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, BUFFER_SIZE / 2);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, http_content_header_handler);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, (__bridge void *)self);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, http_curl_control);
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, (__bridge void *)self);
|
||||
// enable up to 10 redirects
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
|
||||
headers = curl_slist_append(headers, "Icy-Metadata:1");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTP200ALIASES, ok_aliases);
|
||||
if(pos > 0 && length >= 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_RESUME_FROM, (long)pos);
|
||||
}
|
||||
// fp->status = STATUS_INITIAL;
|
||||
DLog(@"curl: calling curl_easy_perform (status=%d)...\n", self->status);
|
||||
gettimeofday(&last_read_time, NULL);
|
||||
status = curl_easy_perform(curl);
|
||||
DLog(@"curl: curl_easy_perform retval=%d\n", status);
|
||||
if(status != 0) {
|
||||
DLog(@"curl error:\n%s\n", http_err);
|
||||
}
|
||||
[mutex lock];
|
||||
if(self->status != STATUS_SEEK) {
|
||||
DLog(@"curl: break loop\n");
|
||||
[mutex unlock];
|
||||
break;
|
||||
} else {
|
||||
DLog(@"curl: restart loop\n");
|
||||
skipbytes = 0;
|
||||
self->status = STATUS_INITIAL;
|
||||
DLog(@"seeking to %lld\n", pos);
|
||||
if(length < 0) {
|
||||
// icy -- need full restart
|
||||
pos = 0;
|
||||
content_type = nil;
|
||||
seektoend = 0;
|
||||
gotheader = 0;
|
||||
icyheader = 0;
|
||||
gotsomeheader = 0;
|
||||
wait_meta = 0;
|
||||
icy_metaint = 0;
|
||||
}
|
||||
}
|
||||
[mutex unlock];
|
||||
curl_slist_free_all(headers);
|
||||
curl_slist_free_all(ok_aliases);
|
||||
}
|
||||
self->curl = NULL;
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
[mutex lock];
|
||||
if(self->status == STATUS_ABORTED) {
|
||||
DLog(@"curl: thread ended due to abort signal");
|
||||
} else {
|
||||
DLog(@"curl: thread ended normally");
|
||||
self->status = STATUS_FINISHED;
|
||||
}
|
||||
[mutex unlock];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSURL *)url {
|
||||
didReceiveResponse = NO;
|
||||
didReceiveRandomData = NO;
|
||||
redirected = NO;
|
||||
taskSuspended = NO;
|
||||
|
||||
redirectURLs = [[NSMutableArray alloc] init];
|
||||
bufferedData = [[NSMutableData alloc] init];
|
||||
|
||||
URL = url;
|
||||
[redirectURLs addObject:URL];
|
||||
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:url];
|
||||
session = [self createSession];
|
||||
task = [session dataTaskWithRequest:request];
|
||||
[task resume];
|
||||
mutex = [[NSLock alloc] init];
|
||||
|
||||
while(task && !didReceiveResponse)
|
||||
usleep(1000);
|
||||
need_abort = NO;
|
||||
|
||||
if(!task && !didReceiveResponse) return NO;
|
||||
status = STATUS_UNSTARTED;
|
||||
|
||||
pos = 0;
|
||||
length = 0;
|
||||
remaining = 0;
|
||||
skipbytes = 0;
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
nheaderpackets = 0;
|
||||
content_type = nil;
|
||||
curl = NULL;
|
||||
memset(&last_read_time, 0, sizeof(last_read_time));
|
||||
icy_metaint = 0;
|
||||
wait_meta = 0;
|
||||
|
||||
memset(&metadata, 0, sizeof(metadata));
|
||||
metadata_size = 0;
|
||||
metadata_have_size = 0;
|
||||
|
||||
memset(&http_err, 0, sizeof(http_err));
|
||||
|
||||
need_abort = NO;
|
||||
|
||||
album = @"";
|
||||
artist = @"";
|
||||
title = @"";
|
||||
genre = @"";
|
||||
gotmetadata = 0;
|
||||
|
||||
seektoend = 0;
|
||||
gotheader = 0;
|
||||
icyheader = 0;
|
||||
gotsomeheader = 0;
|
||||
|
||||
[NSThread detachNewThreadSelector:@selector(threadEntry:) toTarget:self withObject:nil];
|
||||
|
||||
while(status == STATUS_UNSTARTED) {
|
||||
usleep(3000);
|
||||
}
|
||||
|
||||
while(status != STATUS_READING && curl) {
|
||||
usleep(3000);
|
||||
}
|
||||
|
||||
if(!curl)
|
||||
return NO;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)mimeType {
|
||||
DLog(@"Returning mimetype! %@", _mimeType);
|
||||
return _mimeType;
|
||||
DLog(@"Returning mimetype! %@", content_type);
|
||||
return content_type;
|
||||
}
|
||||
|
||||
- (BOOL)hasMetadata {
|
||||
BOOL ret = !!gotmetadata;
|
||||
gotmetadata = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (NSDictionary *)metadata {
|
||||
return @{ @"genre": genre, @"album": album, @"artist": artist, @"title": title };
|
||||
}
|
||||
|
||||
- (BOOL)seekable {
|
||||
return NO;
|
||||
return length > 0;
|
||||
}
|
||||
|
||||
- (BOOL)seek:(long)position whence:(int)whence {
|
||||
return NO;
|
||||
seektoend = 0;
|
||||
if(whence == SEEK_END) {
|
||||
if(position == 0) {
|
||||
seektoend = 1;
|
||||
return 0;
|
||||
}
|
||||
DLog(@"curl: can't seek in curl stream relative to EOF");
|
||||
return NO;
|
||||
}
|
||||
[mutex lock];
|
||||
if(whence == SEEK_CUR) {
|
||||
whence = SEEK_SET;
|
||||
position = pos + position;
|
||||
}
|
||||
if(whence == SEEK_SET) {
|
||||
if(pos == position) {
|
||||
skipbytes = 0;
|
||||
[mutex unlock];
|
||||
return 0;
|
||||
} else if(pos < position && pos + BUFFER_SIZE > position) {
|
||||
skipbytes = position - pos;
|
||||
[mutex unlock];
|
||||
return 0;
|
||||
} else if(pos - position >= 0 && pos - position <= BUFFER_SIZE - remaining) {
|
||||
skipbytes = 0;
|
||||
remaining += pos - position;
|
||||
pos = position;
|
||||
[mutex unlock];
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// reset stream, and start over
|
||||
http_stream_reset(self);
|
||||
pos = position;
|
||||
status = STATUS_SEEK;
|
||||
[mutex unlock];
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (long)tell {
|
||||
return _byteCount;
|
||||
if(seektoend) {
|
||||
return length;
|
||||
}
|
||||
return pos + skipbytes;
|
||||
}
|
||||
|
||||
- (long)read:(void *)buffer amount:(long)amount {
|
||||
@synchronized(bufferedData) {
|
||||
if(didComplete && ![bufferedData length])
|
||||
return 0;
|
||||
}
|
||||
|
||||
long totalRead = 0;
|
||||
long bytesBuffered = 0;
|
||||
|
||||
while(totalRead < amount) {
|
||||
NSData *dataBlock = nil;
|
||||
NSUInteger copySize = amount - totalRead;
|
||||
@synchronized(bufferedData) {
|
||||
if([bufferedData length]) {
|
||||
if(copySize > [bufferedData length])
|
||||
copySize = [bufferedData length];
|
||||
dataBlock = [bufferedData subdataWithRange:NSMakeRange(0, copySize)];
|
||||
- (long)read:(void *)ptr amount:(long)amount {
|
||||
size_t sz = amount;
|
||||
while((remaining > 0 || (status != STATUS_FINISHED && status != STATUS_ABORTED)) && sz > 0) {
|
||||
// wait until data is available
|
||||
while((remaining == 0 || skipbytes > 0) && status != STATUS_FINISHED && status != STATUS_ABORTED) {
|
||||
// DLog(@"curl: readwait, status: %d..\n", status);
|
||||
[mutex lock];
|
||||
if(status == STATUS_READING) {
|
||||
struct timeval tm;
|
||||
gettimeofday(&tm, NULL);
|
||||
float sec = tm.tv_sec - last_read_time.tv_sec;
|
||||
if(sec > TIMEOUT) {
|
||||
DLog(@"http_read: timed out, restarting read");
|
||||
memcpy(&last_read_time, &tm, sizeof(struct timeval));
|
||||
http_stream_reset(self);
|
||||
status = STATUS_SEEK;
|
||||
[mutex unlock];
|
||||
album = @"";
|
||||
artist = @"";
|
||||
title = @"";
|
||||
genre = @"";
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!dataBlock) {
|
||||
@synchronized(task) {
|
||||
if(!task || didComplete) return totalRead;
|
||||
int64_t skip = MIN(remaining, skipbytes);
|
||||
if(skip > 0) {
|
||||
// DLog(@"skipping %lld bytes\n", skip);
|
||||
pos += skip;
|
||||
remaining -= skip;
|
||||
skipbytes -= skip;
|
||||
}
|
||||
usleep(1000);
|
||||
continue;
|
||||
[mutex unlock];
|
||||
usleep(3000);
|
||||
}
|
||||
NSInteger amountReceived = [dataBlock length];
|
||||
if(amountReceived <= 0) {
|
||||
break;
|
||||
// DLog(@"buffer remaining: %d\n", remaining);
|
||||
[mutex lock];
|
||||
// DLog(@"http_read %lld/%lld/%d\n", pos, length, remaining);
|
||||
size_t cp = MIN(sz, remaining);
|
||||
int64_t readpos = pos & BUFFER_MASK;
|
||||
size_t part1 = BUFFER_SIZE - readpos;
|
||||
part1 = MIN(part1, cp);
|
||||
// DLog(@"readpos=%d, remaining=%d, req=%d, cp=%d, part1=%d, part2=%d\n", readpos, remaining, sz, cp, part1, cp-part1);
|
||||
memcpy(ptr, buffer + readpos, part1);
|
||||
remaining -= part1;
|
||||
pos += part1;
|
||||
sz -= part1;
|
||||
ptr += part1;
|
||||
cp -= part1;
|
||||
if(cp > 0) {
|
||||
memcpy(ptr, buffer, cp);
|
||||
remaining -= cp;
|
||||
pos += cp;
|
||||
sz -= cp;
|
||||
ptr += cp;
|
||||
}
|
||||
|
||||
const void *dataBytes = [dataBlock bytes];
|
||||
memcpy(((uint8_t *)buffer) + totalRead, dataBytes, amountReceived);
|
||||
|
||||
@synchronized(bufferedData) {
|
||||
[bufferedData replaceBytesInRange:NSMakeRange(0, amountReceived) withBytes:NULL length:0];
|
||||
_bytesBuffered -= amountReceived;
|
||||
bytesBuffered = _bytesBuffered;
|
||||
}
|
||||
|
||||
if(!didComplete && taskSuspended && bytesBuffered <= (BUFFER_SIZE * 3 / 4)) {
|
||||
[task resume];
|
||||
taskSuspended = NO;
|
||||
}
|
||||
|
||||
totalRead += amountReceived;
|
||||
[mutex unlock];
|
||||
}
|
||||
|
||||
_byteCount += totalRead;
|
||||
|
||||
return totalRead;
|
||||
if(status == STATUS_ABORTED) {
|
||||
return 0;
|
||||
}
|
||||
return amount - sz;
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
if(task) [task cancel];
|
||||
task = nil;
|
||||
|
||||
_mimeType = nil;
|
||||
need_abort = YES;
|
||||
content_type = nil;
|
||||
while(curl != NULL) {
|
||||
usleep(3000);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
1716093A0F627F02008FA424 /* HTTPSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 171609390F627F02008FA424 /* HTTPSource.m */; };
|
||||
8356BD1827B3B7340074E50C /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8356BD1727B3B7340074E50C /* libcurl.tbd */; };
|
||||
8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -31,6 +32,7 @@
|
|||
17ADB60C0B97A74800257CA2 /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = HTTPSource.h; sourceTree = "<group>"; };
|
||||
17ADB6340B97A8B400257CA2 /* Plugin.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = Plugin.h; path = ../../Audio/Plugin.h; sourceTree = SOURCE_ROOT; };
|
||||
32DBCF630370AF2F00C91783 /* HTTPSource_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPSource_Prefix.pch; sourceTree = "<group>"; };
|
||||
8356BD1727B3B7340074E50C /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; };
|
||||
8384912F1808180000E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
|
||||
8D5B49B6048680CD000E48DA /* HTTPSource.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HTTPSource.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -42,6 +44,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8356BD1827B3B7340074E50C /* libcurl.tbd in Frameworks */,
|
||||
8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -57,6 +60,7 @@
|
|||
089C167CFE841241C02AAC07 /* Resources */,
|
||||
089C1671FE841209C02AAC07 /* Frameworks and Libraries */,
|
||||
19C28FB8FE9D52D311CA2CBB /* Products */,
|
||||
8356BD1627B3B7340074E50C /* Frameworks */,
|
||||
);
|
||||
name = HTTPSource;
|
||||
sourceTree = "<group>";
|
||||
|
@ -123,6 +127,14 @@
|
|||
name = "Other Sources";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8356BD1627B3B7340074E50C /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8356BD1727B3B7340074E50C /* libcurl.tbd */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
Loading…
Reference in New Issue