Apr 11 2008

MyTube, now with moving images

Categories: Programming
Tags: ,

So, instead of doing the sensible thing and getting some sleep I decided to try to figure out how to play videos from YouTube. This was a bit tricker then the other pieces as the example application doesn’t actually play videos that I saw. A lot of digging, and hair pulling, later I got something working. Kinda. Mostly. It seems some of the video feeds don’t play at the moment and some play, well, the wrong video, heh. But, I figured it works so I’ll put up how I got it to work.

First, since I need access to the video object I changed the MyTubeIKBrowserItem to hold a GDataEntryYouTubeVideo and modified the constructor to be: - (id)init:theImageID image:(NSImage *)theImage video:(GDataEntryYouTubeVideo *)theVideo;. I then added a simple method to return the video and named it, oddly enough, video.

Once I had that updated I fixed up the code in MyTubeAppController to pass in the YouTube video object when needed.

I figured, since most of the videos appear to be shockwave flash, the easiest way to play them was to embed WebKit. To that end, in the MyTubeAppController.h file I imported import <WebKit/WebView.h> and added another IBOutlet WebView *web; member. Don’t forget to add the WebKit framework to your Linked Frameworks.

With that done double click on MainMenu.nib to launch Interface Builder. In the Library go to Web Kit and drag the Web View to your application window. Make sure you set everything to resize correctly. I gave the WebView and the Image Browser about half the window each to start and made the WebView expand both horizontally and vertically while the Image Browser only expands horizontally. In the control window ctrl-click on the My Tube object and drag from the web circle down to the new WebView widget. That’s it for the Interface Builder portion of our show, close it down and go back to XCode.

After fixing the code for the MyTubeIKBrowserItem changes I only had to add one function:

-(void)imageBrowser:(IKImageBrowserView *)imageBrowser cellWasDoubleClickedAtIndex:(NSUInteger)index {
    MyTubeIKBrowserItem *item = [imageList objectAtIndex:index];
    GDataEntryYouTubeVideo *video = [item video];

    NSString *url = [[[[video mediaGroup] mediaContents] objectAtIndex:0] URLString];
    [web setMainFrameURL:url];
    [web reload:nil];
}

This is another method for the Image Browser. In this case when an item is double clicked this function will be run. All I’m doing is grabbing the item from our imageList and pulling the GDataEntryYouTubeVideo object out of it. Then using this video object I get the mediaGroup then the mediaContents. The mediaContents is an array so I grab the first element. (I have no idea if this is 100% correct but it seems to work). Then, using this item I call the URLString method to get the URL.

I pass the URL to our freshly created web view and tell it to reload.

Volia. You should, hopefully, see the video playing in your webview. If the video doesn’t play, after a few seconds to let it load, try another one. For some reason some of the videos don’t play for me. (Some also cause QuickTime to launch so it’s probably based on video type?)

There you have it. A simple YouTube application to pull down and play videos. It probably leaks like a sieve and I don’t know if it’s 100% correct, but it does work.

This concludes our regularly scheduled MyTube broadcasts.

Full listing of MyTubeAppController follows:

#import "MyTubeAppController.h"
#import "MyTubeIKBrowserItem.h"
#import "GData/GData.h"

@interface MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService;
- (void)fetchEntryImageURLString:(NSString *)urlString withVideo:(GDataEntryYouTubeVideo *)video;
@end

@implementation MyTubeAppController
- (void)awakeFromNib
{
    NSArray *feedTypes = [NSArray arrayWithObjects:
            @"Most Discussed",
            @"Most Linked",
            @"Most Responded",
            @"Most Viewed",
            @"Top Favorites",
            @"Recently Featured",
            nil];

    /* set the items into the field types combo box */
    [feed_type removeAllItems];
    [feed_type addItemsWithTitles:feedTypes];
    [feed_type selectItemWithTitle:@"Most Viewed"];

    imageList = [[NSMutableArray alloc] initWithCapacity:20];
    [browser setDelegate:self];
    [browser setDataSource:self];
}

- (IBAction)grab:(id)sender
{
    GDataServiceGoogleYouTube *service = [self youTubeService];
    GDataServiceTicket *ticket;

    NSString *feedName = [[feed_type selectedItem] title];
    feedName = [feedName lowercaseString];
    feedName = [feedName stringByReplacingOccurrencesOfString:@" " withString:@"_"];

    NSURL *feedURL = [GDataServiceGoogleYouTube youTubeURLForFeedID:feedName];

    ticket = [service fetchYouTubeFeedWithURL:feedURL
                                     delegate:self
                            didFinishSelector:@selector(entryListFetchTicket:finishedWithFeed:)
                              didFailSelector:@selector(entryListFetchTicket:failedWithError:)];

    [imageList removeAllObjects];
}

/* these two functions are for handling the response from fetching a feed */
- (void)entryListFetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed {
    GDataFeedYouTubeVideo *vfeed = (GDataFeedYouTubeVideo *)feed;
    int i;

    for (i = 0; i < [[vfeed entries] count]; i++) {
        GDataEntryBase *entry = [[vfeed entries] objectAtIndex:i];
        if (![entry respondsToSelector:@selector(mediaGroup)]) continue;

        GDataEntryYouTubeVideo *video = (GDataEntryYouTubeVideo *)entry;
        NSArray *thumbnails = [[video mediaGroup] mediaThumbnails];
        if ([thumbnails count] == 0) continue;

        NSString *imageURLString = [[thumbnails objectAtIndex:0] URLString];
        [self fetchEntryImageURLString:imageURLString withVideo:video];
    }
}

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket failedWithError:(NSError *)error {
    NSLog(@"Error %@", error);
}

/* These three functions handle the responses for fetching an image */
- (void)imageFetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data {
    NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
    GDataEntryYouTubeVideo *video = [fetcher userData];

    [imageList addObject:[[MyTubeIKBrowserItem alloc] init:[[video title] stringValue] image:image video:video]];
    [browser reloadData];
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data {
    NSString *dataStr = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    NSLog(@"image fetch error %d with data %@", status, dataStr);
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error {
    NSLog(@"Image fetch error %@", error);
}

/* these two functions are for the image browser delegate */
- (NSUInteger)numberOfItemsInImageBrowser:(IKImageBrowserView *)imageBrowser {
	return [imageList count];
}

- (id)imageBrowser:(IKImageBrowserView *)imageBrowser itemAtIndex:(NSUInteger)index {
	return [imageList objectAtIndex:index];
}

-(void)imageBrowser:(IKImageBrowserView *)imageBrowser cellWasDoubleClickedAtIndex:(NSUInteger)index {
    MyTubeIKBrowserItem *item = [imageList objectAtIndex:index];
    GDataEntryYouTubeVideo *video = [item video];

    NSString *url = [[[[video mediaGroup] mediaContents] objectAtIndex:0] URLString];
    [web setMainFrameURL:url];
    [web reload:nil];
}
@end

@implementation MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService {
    static GDataServiceGoogleYouTube *service = nil;

    if (!service) {
        service = [[GDataServiceGoogleYouTube alloc] init];
        [service setUserAgent:@"MyTube-1.0"];
        [service setShouldCacheDatedData:YES];

        /* this is where we'd set the username/password if we accepted one */
        [service setUserCredentialsWithUsername:nil password:nil];
    }

    return service;
}

/* fetch a specific image URL from YouTube */
- (void)fetchEntryImageURLString:(NSString *)urlString withVideo:(GDataEntryYouTubeVideo *)video {
    if (!urlString) return;

    NSURL *imageURL = [NSURL URLWithString:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    GDataHTTPFetcher *fetcher = [GDataHTTPFetcher httpFetcherWithRequest:request];
    [fetcher setUserData:video];

    [fetcher beginFetchWithDelegate:self
                  didFinishSelector:@selector(imageFetcher:finishedWithData:)
          didFailWithStatusSelector:@selector(imageFetcher:failedWithStatus:data:)
           didFailWithErrorSelector:@selector(imageFetcher:failedWithError:)];
}
@end

Apr 10 2008

MyTube, installing the image wall

Categories: Programming
Tags: ,

Ok, last time we got the groundwork laid for MyTube. It doesn’t do anything extremely exciting but we can connect to YouTube and grab some information. In this installment we’re going to take the information returned from YouTube and for each video draw the thumbnail into our window.

We’re going to start off by adding another class in order to hold the image browser items. To do this ctrl-click on the Classes folder and select Add -> New File. Select the Objective-C class entry and hit next. Name the class MyTubeIKBrowserItem.

Edit the MyTubeIKBrowserItem.h file and make it look similar to the following:

#import <Cocoa/Cocoa.h>

@interface MyTubeIKBrowserItem : NSObject {
    NSString *imageID;
    NSImage *image;
}
- (id)init:theImageID image:(NSImage *)theImage;
- (NSString *)imageUID;
- (NSString *)imageRepresentationType;
- (id)imageRepresentation;
- (NSString *)imageTitle;
@end

These methods allow us to implement the protocol for an IKImageBrowserItem. The imageTitle function is optional but I like seeing the titles in my image view.

With that done, open the MyTubeIKBrowserImage.m file and paste the following:

#import "MyTubeIKBrowserItem.h"
#import <Quartz/Quartz.h>

@implementation MyTubeIKBrowserItem
- (id)init:theImageID image:(NSImage *)theImage {
    if (self = [super init]) {
        image = [theImage copy];
        imageID = [[theImageID lastPathComponent] copy];
    }
    return self;
}

- (NSString *)imageUID {
    return imageID;
}

- (NSString *)imageRepresentationType {
    return IKImageBrowserNSImageRepresentationType;
}

- (id)imageRepresentation {
    return image;
}

- (NSString*)imageTitle {
    return imageID;
}
@end

You’ll notice we’re including the Quartz headers. This is where the definitions for the IKImageBrowserView reside. We’ll need to add the Quartz framework to the project in the same way we added the GData framework in the last tutorial. The implementation of MyTubeIKBrowserItem is pretty simplistic. About the only confusing bit is that the image ID is the image title. I just did that as I figured the title would be unique so there was no reason to use a secondary ID. We’re going to use NSImage objects for our images and that’s what we’re saying with the IKImageBrowserNSImageRepresentationType.

You should probably do a test build at this point to make sure everything is in there correctly. Don’t worry, I’ll wait.

Great, we’ve got our foundation in place so let’s get this party started. Open the MyTubeAppController.h file and add the following below the feed_type entry:

IBOutlet IKImageBrowserView *browser;
NSMutableArray *imageList;

Don’t forget to import <Quartz/Quartz.h> at the top of the file.

That’s all we need to do to the .h file. Let’s setup the GUI for our image browser. Open up the MainMenu.nib file in Interface Builder. From the Library select Image Kit and drag the Image Browser View to your window. In the control window ctrl-click on the My Tube object we created last time and drag the circle beside browser to the newly placed Image Browser View.

This will add the component and hook it up to our controller. Before we close Interface Builder lets set a couple of properties up to make things nicer. Select the Image Browser View we just placed in the window. We’re going to embed it into a scroll view as there are usually more thumbnails then we can see on a screen. Select Layout -> Embed Objects In -> Scroll View. Verify in the apple-3 pane of the inspector that everything is going to resize correctly.

Now, with the scroll view selected press apple-1 to go back to the attributes pane of the inspector. Make sure horizontal scrolling is disabled and turn on the auto hiding of the scroll bars. Select the Image Browser View again and in the attributes pane make sure that Titled is selected, Multiple Selection is disabled and I like to enable the Animates option.

That’s it, you can now save and close interface builder.

Open the MyTubeAppController.m file in interface builder. We need to add a bit of initialization for our new array and browser view so go to the awakeFromNib function and add the following at the end:

    imageList = [[NSMutableArray alloc] initWithCapacity:20];
    [browser setDelegate:self];
    [browser setDataSource:self];

We’re creating our initial image list with a capacity of 20 items. Don’t worry, this will grow if needed. For our image browser we set ourselves as the delegate and the data source. We’ll need to implement a couple of methods to handle being the data source for the image browser.

Add the following two methods to the @implementation section of MyTubeAppController.

/* these two functions are for the image browser delegate */
- (NSUInteger)numberOfItemsInImageBrowser:(IKImageBrowserView *)imageBrowser {
	return [imageList count];
}

- (id)imageBrowser:(IKImageBrowserView *)imageBrowser itemAtIndex:(NSUInteger)index {
	return [imageList objectAtIndex:index];
}

All we’re doing is telling the image browser the number of items we want to display and returning each item when queried. That’s it. Other then actually having data items to display our image browser is ready to rock.

Ok, so the next step is to actually retrieve some images from YouTube to show in our browser. To do that we’re going to edit the entryListFetchTicket:finishedWithFeed: method. The final method should look similar to:

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed {
    GDataFeedYouTubeVideo *vfeed = (GDataFeedYouTubeVideo *)feed;
    int i;

    for (i = 0; i < [[vfeed entries] count]; i++) {
        GDataEntryBase *entry = [[vfeed entries] objectAtIndex:i];
        if (![entry respondsToSelector:@selector(mediaGroup)]) continue;

        GDataEntryYouTubeVideo *video = (GDataEntryYouTubeVideo *)entry;
        NSArray *thumbnails = [[video mediaGroup] mediaThumbnails];
        if ([thumbnails count] == 0) continue;

        NSString *imageURLString = [[thumbnails objectAtIndex:0] URLString];
        [self fetchEntryImageURLString:imageURLString withTitle:[[entry title] stringValue]];
    }
}

You can see that in place of just printing the feed title we’re actually doing something useful with the returned data. In this case we first verify that the entry responds to the mediaGroup selector. If this is true then we have a video item. If the entry doesn’t respond to mediaGroup then this isn’t a video object and we just skip it as there won’t be any thumbnails.

Since we know this is a video object we cast it to a GDataEntryYouTubeVideo object. With the video object in hand we call [[video mediaGroup] mediaThumbnails] to first retrieve the media group for the video and then grab any thumbnails for that media group.

If there are no thumbnails present in the group we can skip this video object as there is nothing to display. (Yes, this is probably silly if you want to see everything, I’m only interested in pretty pictures here.)

Once we know we’ve got some thumbnails we generate the URL for the first thumbnail. This is done by using the URLString method of the first thumbnail item. Using the URL string we call fetchEntryImageURLString:withTitle: which we’ll define next.

fetchEntryImageURLString:withTitle: is a private method of the controller class. As such we need to add - (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title; into the @interface MyTubeAppController (Private) section we’ve previously defined at the top of the file.

Then, we’ll put the following implementation into the private @implementation section with the youTubeService method.

/* fetch a specific image URL from YouTube */
- (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title {
    if (!urlString) return;

    NSURL *imageURL = [NSURL URLWithString:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    GDataHTTPFetcher *fetcher = [GDataHTTPFetcher httpFetcherWithRequest:request];
    [fetcher setUserData:title];

    [fetcher beginFetchWithDelegate:self
                  didFinishSelector:@selector(imageFetcher:finishedWithData:)
          didFailWithStatusSelector:@selector(imageFetcher:failedWithStatus:data:)
           didFailWithErrorSelector:@selector(imageFetcher:failedWithError:)];
}

Similar to when we were fetching the original feed we generate a NSURL object. We then use this object to create a NSURLRequest. Our request gets fed into the GDataHTTPFetcher object which will do the actual work. As I want to get access to it later I set the title onto the fetcher as a bit of data. We then use the beingFetchWithDelegate:didFinishSelector:didFailWithStatusSelector:didFailWithErrorSelector: method to initiate the fetch.

With that done all we have to do is implement the final three selectors and we’re finished.

Back in the main @implementation section for MyTubeAppController we add the following:

/* These three functions handle the responses for fetching an image */
- (void)imageFetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data {
    NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];

    [imageList addObject:[[MyTubeIKBrowserItem alloc] init:[fetcher userData] image:image]];
    [browser reloadData];
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data {
    NSString *dataStr = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    NSLog(@"image fetch error %d with data %@", status, dataStr);
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error {
    NSLog(@"Image fetch error %@", error);
}

The last too functions we’re going to skip over. They both deal with error cases and in both cases we just spit the error to the console.

The first function is the one we’re interested in. This will be called when the HTTP fetcher successfully retrieves the data from YouTube. We know the data we’re fetching is an image so we initialize an NSImage object with the provided data. We then take the given object and the image title (pulled out of the data attached to our HTTP fetcher) and create a new MyTubeIKBrowserItem which is added to our image list. Finally, we reload the browser view and we’re done.

If you run the app you should be able to grab the feeds and see the thumbnails appear in the image browser. Pretty simple, eh? Next time we’ll see if we can play a video that we select.

A complete listing of MyTubeAppController.m follows:

#import "MyTubeAppController.h"
#import "MyTubeIKBrowserItem.h"
#import "GData/GData.h"

@interface MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService;
- (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title;
@end

@implementation MyTubeAppController
- (void)awakeFromNib
{
    NSArray *feedTypes = [NSArray arrayWithObjects:
            @"Most Discussed",
            @"Most Linked",
            @"Most Responded",
            @"Most Viewed",
            @"Top Favorites",
            @"Recently Featured",
            nil];

    /* set the items into the field types combo box */
    [feed_type removeAllItems];
    [feed_type addItemsWithTitles:feedTypes];
    [feed_type selectItemWithTitle:@"Most Viewed"];

    imageList = [[NSMutableArray alloc] initWithCapacity:20];
    [browser setDelegate:self];
    [browser setDataSource:self];
}

- (IBAction)grab:(id)sender
{
    GDataServiceGoogleYouTube *service = [self youTubeService];
    GDataServiceTicket *ticket;

    NSString *feedName = [[feed_type selectedItem] title];
    feedName = [feedName lowercaseString];
    feedName = [feedName stringByReplacingOccurrencesOfString:@" " withString:@"_"];

    NSURL *feedURL = [GDataServiceGoogleYouTube youTubeURLForFeedID:feedName];

    ticket = [service fetchYouTubeFeedWithURL:feedURL
                                     delegate:self
                            didFinishSelector:@selector(entryListFetchTicket:finishedWithFeed:)
                              didFailSelector:@selector(entryListFetchTicket:failedWithError:)];

    [imageList removeAllObjects];
}

/* these two functions are for handling the response from fetching a feed */
- (void)entryListFetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed {
    GDataFeedYouTubeVideo *vfeed = (GDataFeedYouTubeVideo *)feed;
    int i;

    for (i = 0; i < [[vfeed entries] count]; i++) {
        GDataEntryBase *entry = [[vfeed entries] objectAtIndex:i];
        if (![entry respondsToSelector:@selector(mediaGroup)]) continue;

        GDataEntryYouTubeVideo *video = (GDataEntryYouTubeVideo *)entry;
        NSArray *thumbnails = [[video mediaGroup] mediaThumbnails];
        if ([thumbnails count] == 0) continue;

        NSString *imageURLString = [[thumbnails objectAtIndex:0] URLString];
        [self fetchEntryImageURLString:imageURLString withTitle:[[entry title] stringValue]];
    }
}

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket failedWithError:(NSError *)error {
    NSLog(@"Error %@", error);
}

/* These three functions handle the responses for fetching an image */
- (void)imageFetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data {
    NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];

    [imageList addObject:[[MyTubeIKBrowserItem alloc] init:[fetcher userData] image:image]];
    [browser reloadData];
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data {
    NSString *dataStr = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    NSLog(@"image fetch error %d with data %@", status, dataStr);
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error {
    NSLog(@"Image fetch error %@", error);
}

/* these two functions are for the image browser delegate */
- (NSUInteger)numberOfItemsInImageBrowser:(IKImageBrowserView *)imageBrowser {
	return [imageList count];
}

- (id)imageBrowser:(IKImageBrowserView *)imageBrowser itemAtIndex:(NSUInteger)index {
	return [imageList objectAtIndex:index];
}
@end

@implementation MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService {
    static GDataServiceGoogleYouTube *service = nil;

    if (!service) {
        service = [[GDataServiceGoogleYouTube alloc] init];
        [service setUserAgent:@"MyTube-1.0"];
        [service setShouldCacheDatedData:YES];

        /* this is where we'd set the username/password if we accepted one */
        [service setUserCredentialsWithUsername:nil password:nil];
    }

    return service;
}

/* fetch a specific image URL from YouTube */
- (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title {
    if (!urlString) return;

    NSURL *imageURL = [NSURL URLWithString:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    GDataHTTPFetcher *fetcher = [GDataHTTPFetcher httpFetcherWithRequest:request];
    [fetcher setUserData:title];

    [fetcher beginFetchWithDelegate:self
                  didFinishSelector:@selector(imageFetcher:finishedWithData:)
          didFailWithStatusSelector:@selector(imageFetcher:failedWithStatus:data:)
           didFailWithErrorSelector:@selector(imageFetcher:failedWithError:)];
}
@end

Apr 10 2008

MyTube, from the ground up

Categories: Programming
Tags: ,

I haven’t had much chance to play with Cocoa in a while and I’ve got an itch to play with it a bit more. I’m actually tackling two things at once, refreshing my Cocoa and playing with the Google GData APIs for Objective-C. I figure why not write it all down as I do it? This will serve to remind me how these things work and maybe help someone else out. As a note, this example is heavily cribbed from the YouTube example that ships with the GData code as such all code is under the same Apache version 2.0 license.

Note that this example is using XCode 3.0. If you’ve got an earlier version of XCode some of these directions maybe different.

So, without further ado, MyTube, the baby steps edition. The goal for MyTube is simple. Get the feed listings from YouTube, display thumbnails and titles for the videos and let the user play the videos. Nothing fancy, and for the first version, no authentication.

Ok, let’s get started.

The first thing you’re going to want to do is grab the GData zip file off of the GData Objective-C Client website. Once you’ve got this in hand, open it in XCode and compile the framework. Once it’s built you can either use the framework in place or copy it to your library path. I had the best luck with copying the framework into my local library path. You can do this in other ways, and the Google examples have a copy script if you want to do it that way. If you want to just stick it in your library path copy the GData.framework directory that was built into your ~/Library/Frameworks directory. My directions that follow will assume you’ve copied the framework into your frameworks directory. If you don’t do this adjust as needed below.

Once the GData framework is installed we’re ready to roll. First we create a new project in XCode. This is as simple as hitting the File -> New Project menu item. I named my project MyTube and that’s what I’ll be referring to and using as my naming convention.

When the project is created we can go ahead and add the GData framework. This is done by expanding the Frameworks section, ctrl-clicking on Linked Frameworks and select Add -> Existing Frameworks. Navigate to your ~/Library/Frameworks/ directory and select GData.framework. Once the framework is linked do a quick apple-R to build the project and make sure it runs.

During my initial fiddling with adding the framework I was getting a strange dyld error about not being able to load an image. My solution was to copy the framework into the ~/Library/Frameworks directory. So, if you didn’t do that and are getting a strange runtime error, try moving the framework and re-adding it.

Now that the groundwork is in place, let’s get on with the actual application development. First, we’ll create an application controller class. Ctrl-click on the Classes directory and select Add -> New File... from the menu. Select Objective-C NSWindowController Subclass from the list and hit next. Enter the name MyTubeAppController as the name of the class and hit finish. You should end up with a .m and a .h file in your classes directory.

Edit the .h file so it’s similar to the following:

#import <Cocoa/Cocoa.h>

@interface MyTubeAppController : NSWindowController {
    IBOutlet NSPopUpButton *feed_type;
}
- (IBAction)grab:(id)sender;
@end

What we’ve done is defining an outlet and action to be used by Interface Building when hooking the GUI to our backend code. The outlet we’ll attach to a popup button we’re going to create and the action we’ll attach to the pressing of a button in the GUI.

Open the MyTubeAppController.m file and edit to so it’s similar to:

#import "MyTubeAppController.h"
#import "GData/GData.h"

@implementation MyTubeAppController
- (void)awakeFromNib {
    NSArray *feedTypes = [NSArray arrayWithObjects:
            @"Most Discussed",
            @"Most Linked",
            @"Most Responded",
            @"Most Viewed",
            @"Top Favorites",
            @"Recently Featured",
            nil];

    /* set the items into the field types combo box */
    [feed_type removeAllItems];
    [feed_type addItemsWithTitles:feedTypes];
    [feed_type selectItemWithTitle:@"Most Viewed"];
}

- (IBAction)grab:(id)sender {
    NSLog(@"GRAB %@", [[feed_type selectedItem] title]);
}
@end

We start out by importing the need GData header files. These will give us access to all of the GData code. We then override the awakeFromNib to setup our popup button. The strings set into the popup are based on the keys we’ll need when doing the YouTube queries. We clear out our popup, set the strings and select the Most Viewed item.

Lastly, we create a simple grab function which just prints the title of the selected popup item when the button is pressed.

Save all of that and double click on the MainMenu.nib file to launch Interface Builder.

In interface builder go to the Objects & Controllers section of the Library and drag the Object block into the MainMenu.nib window. Click on the block and press apple-i to bring up the inspector. Press apple-6 to switch to the identity tab of the inspector. In the class name field enter MyTubeAppController.

With that done drag a popup button and a regular button into the application window. When the items are placed ctrl-click on the controller object we placed earlier. A black popup should appear. Click in the circle beside feed_type and drag to the popup button and release. Click in the circle beside grab and drag to the regular button. We’ve now hooked the interface items in the .nib file to the items defined in our .h file. Hit apple-s to save the nib file.

Switch back to Xcode and press apple-r to build and run the project. You should be able to select items from the popup and when you press the button a log message will appear in the debug console telling you what you selected.

We know the basic app is working so lets actually query some data from YouTube. In order to create the YouTube service I’m going to define a private method inside MyTubeAppController. This is done by adding the following after the initial declarations but before the @implementation entry in MyTubeAppController.m

@interface MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService;
@end

With that out of the way, we can go all the way to the end of the MyTubeAppController.m file, after the @end entry and insert the following: (Yes, this is ripped from the Google example file with minor modifications)

@implementation MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService {
    static GDataServiceGoogleYouTube *service = nil;

    if (!service) {
        service = [[GDataServiceGoogleYouTube alloc] init];
        [service setUserAgent:@"MyTube-1.0"];
        [service setShouldCacheDatedData:YES];

        /* this is where we'd set the username/password if we accepted one */
        [service setUserCredentialsWithUsername:nil password:nil];
    }
    return service;
}
@end

What are we doing here you ask? Well, it’s actually pretty simple we’re creating a static GDataServiceGoogleYouTube object, static because we only want one to exist, and creating that service if needed. We set the user agent string to something unique for our application, in this case MyTube-1.0 and we turn on the data caching feature.

I’m not setting any credentials into the object as I’m just doing stuff anonymously at the moment (I don’t actually have a YouTube account). You can refer to the Google example on how to set the username and password.

With the ability to create our service we’re going to modify our grab function to pull down the feed items that are selected. Modify grab to be similar to the following:

- (IBAction)grab:(id)sender
{
    GDataServiceGoogleYouTube *service = [self youTubeService];
    GDataServiceTicket *ticket;

    NSString *feedName = [[feed_type selectedItem] title];
    feedName = [feedName lowercaseString];
    feedName = [feedName stringByReplacingOccurrencesOfString:@" " withString:@"_"];

    NSURL *feedURL = [GDataServiceGoogleYouTube youTubeURLForFeedID:feedName];
    ticket = [service fetchYouTubeFeedWithURL:feedURL
                                     delegate:self
                            didFinishSelector:@selector(entryListFetchTicket:finishedWithFeed:)
                              didFailSelector:@selector(entryListFetchTicket:failedWithError:)];
}

We’re using the previously defined youTubeService call to get our service. We then need to figure out the feed we want to query. As I mentioned, the names I’m using in awakeFromNib while similar, aren’t quite what YouTube expects. We need to modify the strings to be all lower case and replace any space ( ) characters with an underscore (_).

With the corrected feed name we can then create a feed URL. In this case we’re just using the youTubeURLForFeedID as we don’t have a userID to query with. If you’re doing authentication stuff refer to the Google example to see how to use the youTubeURLForUserID:(NSString *)username userFeedID:(NSString *)feedID function.

Using the service object we create we can now make our query to YouTube. This is done with the fetchYouTubeFeedWithURL:(NSURL *)url delegate:(id)delegate didFinishSelector:selector didFailSelector:selector this will return a GDataServiceTicket object which, if we saved it, could be used to cancel our query or other things.

All we’ve got left to do in this leg of the trip is to define the two selector functions defined above. Add this to the MyTubeAppController.m file after the grab function.

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed {
    GDataFeedYouTubeVideo *vfeed = (GDataFeedYouTubeVideo *)feed;
    int i;

    for (i = 0; i < [[vfeed entries] count]; i++) {
        GDataEntryBase *entry = [[vfeed entries] objectAtIndex:i];
        NSLog(@"Title: %@", [[entry title] stringValue]);
    }
}

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket failedWithError:(NSError *)error {
    NSLog(@"Error %@", error);
}

As you can see, we aren’t doing anything fancy here. In the finishedWithFeed case we cast the provided feed to a GDataFeedYouTubeVideo object. Using this object we then iterate over the entries and print the title of each entry to the console. In the failedWithError case we just print out the error message to the console.

That’s it. If you hit apple-r to build and run the project you should be able to select items in the popup box and press the grab button to see a list of video titles appear in the console window.

Congratulations, you’ve just made your first query to YouTube from Objective-C.

That’s the end of this installment. I need to get some sleep. Hopefully in the next few days I’ll have some time to write part two which will, hopefully, take the videos and draw thumbnail images into our window so we can see pretty pictures.

Full listing for MyTubeAppController.m follows:

#import "MyTubeAppController.h"
#import "GData/GData.h"

@interface MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService;
@end

@implementation MyTubeAppController
- (void)awakeFromNib {
    NSArray *feedTypes = [NSArray arrayWithObjects:
            @"Most Discussed",
            @"Most Linked",
            @"Most Responded",
            @"Most Viewed",
            @"Top Favorites",
            @"Recently Featured",
            nil];

    /* set the items into the field types combo box */
    [feed_type removeAllItems];
    [feed_type addItemsWithTitles:feedTypes];
    [feed_type selectItemWithTitle:@"Most Viewed"];
}

- (IBAction)grab:(id)sender {
    GDataServiceGoogleYouTube *service = [self youTubeService];
    GDataServiceTicket *ticket;

    NSString *feedName = [[feed_type selectedItem] title];
    feedName = [feedName lowercaseString];
    feedName = [feedName stringByReplacingOccurrencesOfString:@" " withString:@"_"];

    NSURL *feedURL = [GDataServiceGoogleYouTube youTubeURLForFeedID:feedName];
    ticket = [service fetchYouTubeFeedWithURL:feedURL
                                     delegate:self
                            didFinishSelector:@selector(entryListFetchTicket:finishedWithFeed:)
                              didFailSelector:@selector(entryListFetchTicket:failedWithError:)];
}

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed {
    GDataFeedYouTubeVideo *vfeed = (GDataFeedYouTubeVideo *)feed;
    int i;

    for (i = 0; i < [[vfeed entries] count]; i++) {
        GDataEntryBase *entry = [[vfeed entries] objectAtIndex:i];
        NSLog(@"Title: %@", [[entry title] stringValue]);
    }
}

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket failedWithError:(NSError *)error {
    NSLog(@"Error %@", error);
}
@end

@implementation MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService {
    static GDataServiceGoogleYouTube *service = nil;

    if (!service) {
        service = [[GDataServiceGoogleYouTube alloc] init];
        [service setUserAgent:@"MyTube-1.0"];
        [service setShouldCacheDatedData:YES];

        /* this is where we'd set the username/password if we accepted one */
        [service setUserCredentialsWithUsername:nil password:nil];
    }
    return service;
}
@end

Apr 07 2008

Queen, richer, step two

Categories: Writing

Stacy’s had her second story published. Got check it out at Fantasy Magazine.

Congrats Stacy.


Apr 06 2008

Out of the fire and burning up

Again, it’s been a while. Once you drop off the blogging it becomes a lot harder to pick it up again. I finally got around to uploading some pictures to flickr that go back to my Seattle trip in January.

Anyway, what have I been up too? Well, there has been a few work based trips and Sahba and I went and spoke at CarolinaCon and NotACon about the Exploit-Me tools. Both cases were a lot of fun. Sadly we had to fly out early on the Saturday so we missed a lot of NotACon. I believe the video of the NotACon talk will be online at somepoint. It was filmed by the guys from Media Archives.


« Previous Page