Friday, May 22, 2009

Replacing the QuickTime logo with a custom image

IMPORTANT NOTE: As of iPhone OS 3.1, this technique no longer works. As I mentioned in the original post, this technique depended on the internal structure of the QuickTime player view hierarchy and could stop working at any time. And, so it has. I leave this post for historical purposes, but it is no longer accurate or valid. Except maybe as a warning to not depend on the internal structure of Apple's classes.

On an iPhone developer forum, someone recently asked whether it is possible to replace the QuickTime 'QT' logo that appears with the iPhone movie player when playing an audio file or stream in an iPhone app. Several people responded saying it is not possible, even though the YouTube app does it. I respectfully disagree. Presented below is a very simple technique which currently will in fact work, and is a technique I am using in an app already available on iTunes.

If you are curious what the end result looks like, take a look at the Hi Tony app on iTunes (link opens iTunes). It's a free download. [ EDIT: the technique I was originally using only works with 2.x of the iPhone OS. A new update to the Hi Tony! app which will work with 3.0 is pending with Apple.]

If you just want to see the code, it is provided below. It assumes a certain view name ("MPVideoBackgroundView") will exist in the movie player class. There's no guarantee Apple won't change this in the future. Use at your own risk.

I should point out that no hidden APIs or function calls are being used, so in theory Apple should not reject your app if you use this technique. Again, however, use at your own risk. As I've mentioned before Apple's review process can be arbitrary and subjective. Just because my app was approved using this technique does not mean yours will.

But, in the interest of "sharing the knowledge", I figured I should make this technique available to any developer that might want to try it themselves. The function for recursively searching an object for its underlying views was based on a simple example from Erica Sadun. As for finding the magical view name of "MPVideoBackgroundView", this just involved a lot of trial and error on my part examining all the subviews of the movie player.

Please note: This is just a rough example to demonstrate the principle. The code as shown below does not clean up after itself memory-wise.

[EDIT: The following code is a lot different than my original post. It has been rewritten to work with iPhone OS 2.x and 3.0]



- (bool) findVideoBackgroundView: (id) aView level: (int) level
{
NSString *name = [[aView class] description];

if ([name isEqualToString:@"MPVideoBackgroundView"]){
CGRect viewRect = CGRectMake(0, 0, 480, 320);
UIView *temp_view = [[UIImageView alloc] initWithFrame:viewRect];
[temp_view setAlpha:1.0];
[temp_view setBackgroundColor:[UIColor blackColor]];

UIImage *img = [UIImage imageNamed:@"Picture 9.png"];

int x_offset = (480 - img.size.width)/2;
int y_offset = (320 - img.size.height)/2;
CGRect titleRect = CGRectMake(x_offset, y_offset, img.size.width, img.size.height);
UIImageView *myImage = [[UIImageView alloc] initWithFrame:titleRect];
[myImage setImage:img];
[temp_view addSubview:myImage];
[aView insertSubview:temp_view atIndex:0];

return true;
}

for (UIView *subview in [aView subviews]){
if ([self findVideoBackgroundView:subview level:(level + 1)]){
return TRUE;
}
}
return FALSE;
}


- (void) monitorMovieWindow
{
// With iPhone OS 3.0, the movie view does not appear immmediately after calling play.
// It could take several seconds before the view appears.
// As a result, we run this thread in the background to look for when the movie player appears.

// There's probably a better way to do this.

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

bool movieViewNotFound = TRUE;
int maxRetry = 40; // try waiting for the movie view to appear for 4 seconds

while(movieViewNotFound && maxRetry > 0){

// According to Apple's MoviePlayer sample code, the movie player should be the second view in the window stack
NSArray *windows = [[UIApplication sharedApplication] windows];
// Locate the movie player window
UIWindow *moviePlayerWindow = [windows objectAtIndex:1];

if ([self findVideoBackgroundView: moviePlayerWindow level:0]){
movieViewNotFound = FALSE;
}else{
[NSThread sleepForTimeInterval:0.1];
-- maxRetry;
}
}
[pool release];
}


...

// Here's how you would make use of the above methods

// create a new player and initialize it with the movie URL path
MPMoviePlayerController* theMovie=[[MPMoviePlayerController alloc] initWithContentURL:[NSURL URLWithString:@"http://blahblah-insert your URL here"]];
[theMovie play];
// Call the method to overlay the QTlogo view with a custom image
[self performSelectorInBackground:@selector(monitorMovieWindow) withObject:nil];


11 comments:

Robert Aubin said...

Thanks a bunch, Brian. I'll likely be using this sometime soon.

Mostly Torn said...

Oh no! Not the competition! :-)

But seriously, I'm glad I could help.

Randy said...

Has anyone got this to work? For me the player controls never show and the previous view (the one I clicked to get to the player) shows while the audio plays.

Mostly Torn said...

It does indeed work - but not with the 3.0 beta OS.

Are you running 2.x?

I'm looking into what might be different in regards to 3.0.

Randy said...

I'm running 2.2.1. Any tips on how I can debug. I've tried different images and even just removing the image altogether and just showing a UIView with a background color but nothing seems to work for me. I appreciate the help.

Mostly Torn said...

I've made a one line change based on observed failure in simulator. This should now work on all versions of 2.x.

Unfortunately, it definitely does not work under 3.x of the OS. Apple has removed access to the movieView and videoViewControll methods, so there currently appears to be no way to get a handle to the views of which the MPMoviePlayerController is composed.

Mostly Torn said...

And in case you didn't notice all my edits in the original post, this now does in fact work with all current versions of the iPhone OS - 2.x and 3.0.

russellquinndemo said...

Hey there,

I'm trying this with the 3.1 beta and it no longer works. It seems as though the injection of the custom view is overwritten later on by the QT image. If I just add a green view, I see it during the fade in and fade out, but it never stays at the top.

Any ideas?

Russell

russellquinndemo said...

Also, I was getting arrayIndexOutOfBounds problems. It seems window instantiating is rather slow.

You should check that windows.count is higher than 1 here:

UIWindow *moviePlayerWindow = [windows objectAtIndex:1];

Prasanna I am the Way I AM said...

Can we use this technique now. why do that codes are striked.? please clear me

Andrew Koransky said...

I was able to replace the QT logo by doing the following. Only works in 3.2 and later though. I've tested on iPod Touch 4.3 and iPad 4.3.

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];

// put a background image in there...
if ([moviePlayerController respondsToSelector:@selector(backgroundView)]) {
moviePlayerController.backgroundView.backgroundColor = [UIColor blackColor];
self.logoImageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Icon@2x~iphone"]] autorelease];
[moviePlayerController.backgroundView addSubview:logoImageView];
}
}

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

if ([moviePlayerController respondsToSelector:@selector(backgroundView)]) {
int x = (moviePlayerController.backgroundView.bounds.size.width - logoImageView.image.size.width) / 2;
int y = (moviePlayerController.backgroundView.bounds.size.height - logoImageView.image.size.height) / 2;
self.logoImageView.frame = CGRectMake(x, y, logoImageView.image.size.width, logoImageView.image.size.height);
}
}


All content copyright © 2009  Brian Stormont, unless otherwise noted.   All rights reserved.