5

I'm using QTKit to progressively download and play an MP3 from a URL. According to this documentation, this is the code I should use to accomplish that:

NSURL *mp3URL = [NSURL URLWithString:@"http://foo.com/bar.mp3"];

NSError *error = nil;
QTMovie *sound = [[QTMovie alloc] initWithURL:mp3URL error:&error];
[sound play];

This works, and does exactly what I want — the MP3 URL is lazily downloaded and starts playing immediately. However, if the URL does not have the ".mp3" path extension, it fails:

NSURL *mp3URL = [NSURL URLWithString:@"http://foo.com/bar"];

NSError *error = nil;
QTMovie *sound = [[QTMovie alloc] initWithURL:mp3URL error:&error];
[sound play];

No error is given, no exception is raised; the duration of the sound is just set to zero, and nothing plays.

The only way I have found to work around this is to force a type by loading the data manually and using a QTDataReference:

NSURL *mp3URL = [NSURL URLWithString:@"http://foo.com/bar"];
NSData *mp3Data = [NSData dataWithContentsOfURL:mp3URL];

QTDataReference *dataReference = 
    [QTDataReference dataReferenceWithReferenceToData:mp3Data
                                                 name:@"bar.mp3"
                                             MIMEType:nil];
NSError *error = nil;
QTMovie *sound = [[QTMovie alloc] initWithDataReference:dataReference error:&error];
[sound play];

However, this forces me to completely download ALL of the MP3 synchronously before I can start playing it, which is obviously undesirable. Is there any way around this?

Thanks.

Edit

Actually, it seems that the path extension has nothing to do with it; the Content-Type is simply not being set in the HTTP header. Even so, the latter code works and the former does not. Anyone know of a way to fix this, without having access to the server?

Edit 2

Anyone? I can't find information about this anywhere, and Google frustratingly now shows this page as the top result for most of my queries...

Community
  • 1
  • 1
Michael
  • 11,612
  • 10
  • 41
  • 43

4 Answers4

0

Just create an instance like this...

QTMovie *aPlayer  = [QTMovie movieWithAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
                                                 fileUrl, QTMovieURLAttribute,
                                                 [NSNumber numberWithBool:YES], QTMovieOpenForPlaybackAttribute,
                                                 /*[NSNumber numberWithBool:YES], QTMovieOpenAsyncOKAttribute,*/
                                                 nil] error:error];
Vassilis
  • 2,878
  • 1
  • 28
  • 43
0

Two ideas. (The first one being a bit hacky):
To work around the missing content type, you could embed a small Cocoa webserver that supplements the missing header field and route your NSURL over that "proxy".

Some Cocoa http server implementations:

The second one would be, to switch to a lower level framework (From QTKit to AudioToolbox).
You'd need more code, but there are some very good resources out there on how to stream mp3 using AudioToolbox.
e.g.: http://cocoawithlove.com/2008/09/streaming-and-playing-live-mp3-stream.html

Personally I'd go with the second option. AudioToolbox isn't as straightforward as QTKit but it offers a clean solution to your problem. It's also available on both - iOS and Mac OS - so you will find plenty of information.

Update:
Did you try to use another initializer? e.g.

+ (id)movieWithAttributes:(NSDictionary *)attributes error:(NSError **)errorPtr

You can insert your URL for the key QTMovieURLAttribute and maybe you can compensate the missing content type by providing other attributes in that dictionary. This open source project has a QTMovie category that contains methods to accomplish similar things: http://vidnik.googlecode.com/svn-history/r63/trunk/Source/Categories/QTMovie+Async.m

Thomas Zoechling
  • 34,177
  • 3
  • 81
  • 112
  • AudioStreamer looks promising, but unfortunately it doesn't seem to work for my use case as I frequently receive an "Unable to configure network read stream." after pausing and then playing in the middle of an MP3. (Hence why I'm looking for a *progressive downloader* instead of a streamer.) – Michael Dec 13 '10 at 13:55
  • Also, “AudioToolbox isn't as straightforward as QTKit", is a bit of an understatement, isn't it? Core Audio is often cited as the [hardest API on OS X](http://www.mikeash.com/pyblog/why-coreaudio-is-hard.html), and I'm certain is one of the easiest to subtly misuse. Since what I'm doing is relatively simplistic, I'd much prefer to leave the heavy lifting up to a higher-level API that "does it right". – Michael Dec 13 '10 at 13:55
  • Yes, Core Audio is hard and I can totally understand that you look into high level APIs first. I updated my original answer with another idea. (Didn't test it though. Can you post some of the problematic URLs?) – Thomas Zoechling Dec 13 '10 at 15:45
  • Thanks for the help and further suggestion; unfortunately I get the same result, though. Here's an example of a "bad" URL: http://meeselet.s3.amazonaws.com/example – Michael Dec 13 '10 at 18:24
0

If you thought weichsel's first solution was hacky, you're going to love this one:

The culprit is the Content-Type header, as you have determined. Had QTKit.framework used Objective-C internally, this would be a trivial matter of overriding -[NSHTTPURLResponse allHeaderFields] with a category of your choosing. However, QTKit.framework (for better or worse) uses Core Foundation (and Core Services) internally. These are both C-based frameworks and there is no elegant way of overriding functions in C.

That said, there is a method, just not a pretty one. Function interposition is even documented by Apple, but seems to be a bit behind the times, compared to the remainder of their documentation.

In essence, you want something along the following lines:

typedef struct interpose_s {
    void *new_func;
    void *orig_func;
} interpose_t;

CFStringRef myCFHTTPMessageCopyHeaderFieldValue (
                                                 CFHTTPMessageRef message,
                                                 CFStringRef headerField
                                                 );

static const interpose_t interposers[] __attribute__ ((section("__DATA, __interpose"))) = {
    { (void *)myCFHTTPMessageCopyHeaderFieldValue, (void *)CFHTTPMessageCopyHeaderFieldValue }
};

CFStringRef myCFHTTPMessageCopyHeaderFieldValue (
                                                 CFHTTPMessageRef message,
                                                 CFStringRef headerField
                                                 ) {
    if (CFStringCompare(headerField, CFSTR("Content-Type"), 0) == kCFCompareEqualTo) {
        return CFSTR("audio/x-mpeg");
    } else {
        return CFHTTPMessageCopyHeaderFieldValue(message, headerField);
    }
}

You might want to add logic specific to your application in terms of handling the Content-Type field lest your application break in weird and wonderful ways when every HTTP request is determined to be an audio file.

Community
  • 1
  • 1
Aidan Steele
  • 10,999
  • 6
  • 38
  • 59
0

Try replacing http:// with icy://.