diff --git a/Plugins/HTTPSource/HTTPSource.h b/Plugins/HTTPSource/HTTPSource.h index d6821ea74..2bf1934c8 100644 --- a/Plugins/HTTPSource/HTTPSource.h +++ b/Plugins/HTTPSource/HTTPSource.h @@ -10,29 +10,64 @@ #import "Plugin.h" -@class HTTPConnection; +#include -@interface HTTPSource : NSObject { - 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 { 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 diff --git a/Plugins/HTTPSource/HTTPSource.m b/Plugins/HTTPSource/HTTPSource.m index d3c3c6cc6..e4af0cd85 100644 --- a/Plugins/HTTPSource/HTTPSource.m +++ b/Plugins/HTTPSource/HTTPSource.m @@ -14,269 +14,723 @@ #import #import -#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 { diff --git a/Plugins/HTTPSource/HTTPSource.xcodeproj/project.pbxproj b/Plugins/HTTPSource/HTTPSource.xcodeproj/project.pbxproj index 56336125a..213bbf9f1 100644 --- a/Plugins/HTTPSource/HTTPSource.xcodeproj/project.pbxproj +++ b/Plugins/HTTPSource/HTTPSource.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -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 = ""; @@ -123,6 +127,14 @@ name = "Other Sources"; sourceTree = ""; }; + 8356BD1627B3B7340074E50C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8356BD1727B3B7340074E50C /* libcurl.tbd */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */