Sunday, February 25, 2007

QTMovie Tips

Thanks to the QTKit, playing songs in Cocoa has become rather trivial. But there are a few methods that are less than trivial, and other methods that Apple simply left out.

One common thing a developer may want to know: is the song playing? You would imagine there would be a "isPlaying" method that returns a BOOL, but unfortunately there's not. Instead, use the rate, and write your own method like this:



- (BOOL)isPlaying
{
return (movie != nil) && ([movie rate] != 0);
}



Moving on to something a little more complicated: playing songs over the internet. It seems easy enough... Just use the initWithURL:error: method and then call the play method. But when you do this the song doesn't play! What's going on here? Well, when you immediately call play, QuickTime essentially says, "Play what? I haven't downloaded any data yet." So the trick is to listen to the load state, and immediately call the play method when QuickTime has stated that it's downloaded enough of the song to start playing it. Like this:



// First you need to register for the proper notifications
// Probably put this in your init method
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(movieLoadStateDidChange:)
name:QTMovieLoadStateDidChangeNotification
object:nil];

// Create our QTMovie
movie = [[QTMovie alloc] initWithURL:myURL error:nil];

// Immediately call play
// This will only work if the song at myURL is cached
[movie play];

- (void)movieLoadStateDidChange:(NSNotification *)notification
{
// First make sure that this notification is for our movie.
if([notification object] == movie)
{
if([movie rate] == 0)
{
if([[movie attributeForKey:QTMovieLoadStateAttribute] longValue] >= kMovieLoadStatePlaythroughOK)
{
[movie play];
}
}
}



For more information on the load state see the document titled "Monitoring the Load State." It should be right in the Xcode documentation.

What if we want to know how much of the song has been played? This one's not that hard, you just have to know what a QTTime variable is. I find it's helpful to add comments defining what these hard-to-remember types are.



- (float)playProgress
{
if(movie != nil)
{
// typedef struct { long long timeValue; long timeScale; long flags; } QTTime
QTTime qtCurrentTime = [movie currentTime];
QTTime qtDuration = [movie duration];

long long currentTime = qtCurrentTime.timeValue;
long long duration = qtDuration.timeValue;

if(duration > 0)
return ((float)currentTime) / ((float)duration);
else
return 0;
}
else {
return 0;
}
}



What if we want to know how much of the song has been loaded? You might guess it has something to do with the load state. But then you obviously haven't read the "Monitoring the Load State" document that I told you about, and you'd be wrong. :) (Hey, that's what I thought at first too!) This functionality is availble but it's not in QTKit, it's in QuickTime. We're looking for the GetMaxLoadedTimeInMovie method. Here's a method that returns the load progress as a percentage:



- (float)loadProgress
{
if(movie != nil)
{
// typedef long TimeValue;
TimeValue loadProgress;
GetMaxLoadedTimeInMovie([movie quickTimeMovie], &loadProgress);

// typedef struct { long long timeValue; long timeScale; long flags; } QTTime
QTTime qtDuration = [movie duration];
long long duration = qtDuration.timeValue;

if(duration > 0)
return ((float)loadProgress) / ((float)duration);
else
return 0;
}
else {
return 0;
}
}



And speaking of playing songs over the Internet, how would you like to connect to the music libraries of your friends, play their music, and download any songs you want to your computer? Sound cool? Check out Mojo!

11 comments:

hjaltij said...

Nice post.
I went through all this stuff when writing Peel (getpeel.com).
Didn't know about the load progress though, I really could use that.

Thanks!

Scott Stevenson said...

This is great stuff but could you adjust the template to make the font size for the code bigger? It's so small that the wavelength of light is becoming a limitation.

Robbie Hanson said...

Thanks for the suggestion Scott. I've made the font more readable now.

Chris Ryland said...

Great stuff!

if([movie rate] == 0)

might more clearly be expressed as

if (! [movie isPlaying])

given that you've defined it nicely.

Abdul Majeed said...

Nice article, Thanks for publishing this useful one. After reading this, I thought of discussing an issue that I got stuck up in one of my project.I dont know is this a right place to discuss, kindly sorry if it is not.

I am currently working on a client-server based application which got a feature similar to iTunes sharing. That is I want to share movie files between machines over the LAN. Currently on server side, I have created a socket , write the movie data to that socket. On client side I am saving the remote movie data to the a temporary file and giving this local file as input to QuickTime. But it got an issue with QuickTIme/QTMovie updating the movie play if all of the data is not in place.
i.e QuickTime not plalying with complete data.
Could you please let us know Is there any way that I can do stream movie data into a QTMovie/QuickTime. As I explained above the scenario is that I have movie data streaming in over a socket and I'd like to feed that data into a QTMovie/QuickTime and start playing the movie before all of the data is in place.
As I am totally new to QuickTime/QTmovie programing , kindly correct me if I am doing something wrong.
Any help in this regard is greatly appreciated.

Thanks In Advance,
Abdul Majeed K

Robbie Hanson said...

Hi Abdul Majeed,

I do believe it's possible to feed raw data into a QTMovie object somehow, but I've never been able to get it to work. Instead, I usually init a QTMovie with a URL. Is it possible, with your app, to simply stream the data directly from QT? Or are you using something other than HTTP?

Anonymous said...

Hi Robbie hanson,
Thank you very much for your response.

No, it is not possible, with our app, to simply stream the data directly from QT. And I am sure we are also not using HTTP. I hope it would be better if I refer this as file tranfer rather than streaming.
We are trying to do a scenario similar to iTunes/Mojo movie sharing. Let me explain the scenario with code snippet as

One server side ,there will be a file handle listening for incoming connection through the socket, once the connection is accepted following notification function will be called by filehandle to write the movie data to the socket.

- (void)handleStreamingRequestNotification:(NSNotification *)inNote
{
[[inNote object ]acceptConnectionInBackgroundAndNotify];

NSFileHandle *incomingConnection = [[inNote userInfo]objectForKey: NSFileHandleNotificationFileHandleItem];

NSData *movieData = NULL;
unsigned int dataLength = 0;
NSFileHandle *hadleToFile = //input file
// loop while (eofile)
movieData = [hadleToFile readDataOfLength:MAXREADDATASIZE ];
dataLength = [movieData length ];
[incomingConnection writeData: movieData];

//end of the loop
[incomingConnection closeFile];
}


On the client side,
we connect to server socket through api
NSHost *host = [NSHost hostWithName:@"server.name.here.com"];
[NSStream getStreamsToHost:host port:serverport inputStream:&inStream outputStream:nil];
[inStream setDelegate:self];
[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inStream open];//it will contact the server socket.

//Inside the stream:handleEvent: callback of inStream we are creating temporary file using writeToFile api, and giving this local file as input to QTMovie/QuickTime.As I discussed in my previous post, QuickTime is not updating the movie play if the complete data is not in place for some of the movie files.

While looking into the internal structure of Mojo/iTunes app, it is clear that they are also creating a server socket and writing the data to that, but I dont know using what? Also in the client side , Mojo app/iTunes uses API ,QTMovie initWithAttrib..to play the movie file. I am not sure how to specify remote file as input to this , in my above case.

Could you please, let us know any input or any alternate way that I supposed to look in?
I am new to concepts like http, streaming etc , kindly explain enough.

Thanks And Regards,
Abdul Majeed K

shinu said...

Hello there,

I have been working on a application which play the remote movie by using QuickTime, but I have stuck up an issue with QuickTime movie playable state. In following code snippet, the movie load state never changed to kMovieLoadStatePlayable for file types like .mpg and avi. but at the same time It is working well for movie files like .mp4, .mov etc. Could anybody there please help me out in this.





//Code Snippet--------------------------------------------------------------

// Input url const char url[] = "http://100.100.31.196:4660/1.mpg/0";

//get the theMovie

err = NewMovieFromDataRef(&theMovie,
newMovieActive + newMovieAsyncOK ,
nil,
dataRef,
URLDataHandlerSubType);
do
{
long newLoadState = 0;
// Get new load state to see if there was a change in
// state.
newLoadState = GetMovieLoadState(theMovie);
TimeValue val = 0;
OSErr error = ::GetMaxLoadedTimeInMovie(theMovie ,&val);
if (newLoadState != loadState)
{
loadState = newLoadState;
if (loadState < 0)
{
// failed to load the movie - this will cause
// us to drop out of this loop and report an
// error
}

if (loadState < kMovieLoadStatePlayable)
{
// we need to keep tasking the movie so it gets
// time to load
MoviesTask(theMovie, 0);
}

if (loadState < kMovieLoadStateComplete)
{
// we just became playable
}

if (loadState >= kMovieLoadStateComplete)
{
// now we know all media data is available
// this will drop us out of this loop so we
// can display the movie
}
}
}
while ((loadState > kMovieLoadStateError) && (loadState < kMovieLoadStatePlayable));


//----------------------------------------------------


kindly correct me if I am doing something wrong.

Thanks And Regards,
Srinivas

Fernando Valente said...

Nice blog and nice post. Congrats!

I got just one problem. If for example, I stream audio from a radio station. I will need to check if the stream stopped. How may I do that?

Leonardo said...

My very best for your code. My QT movie data is embedded within a file.
I have the offset and length of the data block

mStreamingDict = [NSDictionary dictionaryWithObjectsAndKeys:
mMovieURL, @"QTMovieURLAttribute",
NumYES, @"QTMovieOpenAsyncOKAttribute",
NumNO, @"QTMovieOpenForPlaybackAttribute",
offsetNum, @"QTMovieFileOffsetAttribute",
lengthNum, @"QTMovieDataSizeAttribute",
nil];

mQTMovie = [[QTMovie alloc] initWithAttributes:mStreamingDict error:&error];

When mMovieURL is a local file, it always works.
When the mMovieURL is "the same" file placed on the internet, it never works.
initWithAttributes always reports an error, "The file is not a movie file."
What do I miss?

Victor said...

Hi Robbie,

I've found your question about QTMovie and NSURLCredentialStorage there: http://www.mailinglistarchive.com/quicktime-api@lists.apple.com/msg03080.html

I've faced with same problem and I can't find out how to solve it. Did you find a way how to solve this issue?

Thank you.