Monday, November 2, 2009

Single Table Inheritance and accepts_nested_attributes_for

I recently upgraded our web app at Episodic from Rails 2.0 to Rails 2.3.4. While this resulted in many issues we had to resolve that I will try to write about in later post, it does allow me to take advantage of some of the Rails 2.3.x features like accepts_nested_attributes_for. If you aren’t familiar with accepts_nested_attributes_for then check out Ryan’s post.

So I went ahead and tried to make use of this in my object model. Here are the important parts of the model.


class Episode < ActiveRecord::Base
has_many :field_values, :dependent => :destroy
accepts_nested_attributes_for :field_values, :allow_destroy => true
end

class FieldValue < ActiveRecord::Base
:belongs_to :episode
end

class TextFieldValue < FieldValue
end

class NumberFieldValue < FieldValue
end


Since I am using Single Table Inheritance I end up with a field_values table that has a column named “type” which contain either a value of “TextFieldValue” or “NumberFieldValue”.

Also, because I enabled accepts_nested_attributes_for on my Episode class I should be able to do something like:


episode.field_values_attributes =
[{:value => “foo”, :type => “TextFieldValue”}]


However, when I look in the DB I see that there is a new row but the “type” column is NULL even though I set it to “TextFieldValue”. When I look in the logs I see: "Can't mass-assign these protected attributes: type".

Luckily, there is a way around this. I added a setter called "value_type" to my FieldValue class.


def value_type= value_type
self.type = value_type
end


Now, when I can safely use field_values_attributes and set a type.


episode.field_values_attributes =
[{:value => “foo”, :value_type => “TextFieldValue”}]

Thursday, August 13, 2009

Handling redirects with initWithContentURL

I keep running into this problem so I thought that I should post my solution. There are some classes in the iPhone SDK like the MPMoviePlayerController that have an initWithContentURL method. However, the problem is that the URL I have is one that may issue a redirect response when requested. When I provide such a URL to initWithContentURL the player tells me that it cannot play me file. To work around this problem I wrote some very simple code.

The first thing you need to do is to build a HEAD request using NSMutableURLRequest. Then create an NSURLConnection settings the delegate as the current instance.


NSMutableURLRequest *headRequest =
[NSMutableURLRequest requestWithURL:resourceURL
cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];

[headRequest setHTTPMethod:@"HEAD"];

connection = [[NSURLConnection alloc]
initWithRequest:headRequest delegate:self];


Now we just need to implement the delegate method to play the URL from the response.


- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response {

// Now we have resolved the URL so play the movie
thePlayer = [[MPMoviePlayerController alloc]
initWithContentURL:[response URL]];

[thePlayer play];
}


Obviously, this example handles one redirect but you can see that you can easily have it loop and until [response statusCode] == 200.

Tuesday, June 30, 2009

Streaming and playing fixed-length MP3s using the iPhone SDK

The Problem

Lately, I've been doing a fair amount of coding using Objective-C and the iPhone SDK. Once I got used to the language I really started to like it. The more I used the SDK the more impressed I became with how much I could do with it. However, when a customer of ours at Episodic needed an iPhone App that could play a list of MP3s (RSS Feed to be exact) with "iPod-like" functionality, I was surprised to find that this is not something I can do with the embedded Quicktime Player.  In fact, you can't even just hook into the MPMoviePlayerController.  For example, there is no way to know when one songs ends so that I can start the next song since the MPMoviePlayerPlaybackDidFinishNotification notification is fired when the song ends or the user hits the done button.  So after a lot of searching I came across Matt Gallagher's great post "Revisiting an old post: Streaming and playing an MP3 stream".

Modifying the AudioStreamer Class to Play Fixed-Length MP3s

So Matt's post got me part of the way there.  It at least gave me an understanding of the tools I would need to use to accomplish my task.  The main difference between what he had and what I needed is that he was playing an MP3 stream and I wanted to play MP3 files.  The first change I made was I added a new constructor to the AudioStreamer class.  This allowed me to initialized the class with a URL to a file on the file system (I'll talk about how to get the file on your file system later).

- (id)initWithFileURL:(NSURL *)aURL {
[self initWithURL:aURL];
fixedLength = YES;
return self;
}


This constructor sets a flag to indicate that we are streaming fixed-length MP3s.  I then key off this flag in the openFileStream method.

if (fixedLength) {
stream = CFReadStreamCreateWithFile(
kCFAllocatorDefault, (CFURLRef)url);
} else {
CFHTTPMessageRef message = CFHTTPMessageCreateRequest(
NULL, (CFStringRef)@"GET", (CFURLRef)url,

kCFHTTPVersion1_1);
stream = CFReadStreamCreateForHTTPRequest(NULL, message);
CFRelease(message);
...


The above code simply creates the stream from the file system instead of a remote file. Because the result is still a CFReadStreamRef object the rest of the class mostly remains unchanged.

Downloading the MP3

In my controller I just need to download the MP3 file to a location on the file system.  This can be done very easily by using a combination of NSURLConnection and NSFileHandle.

audioFile = [[NSFileHandle
fileHandleForWritingAtPath:downloadFileName] retain];
NSURLRequest *downloadRequest = [NSURLRequest
requestWithURL:resourceURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
download = [[NSURLConnection alloc]
initWithRequest:downloadRequest delegate:self];


This code creates the file to write the data to and creates the connection to fetch the data.  I know just need to handle the data.

- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data {
// Append the data to the file on the file system
[audioFile writeData:data];
downloadedLength += [data length];

// When we get enough of the file, then just start playing.
if (!streamer && downloadedLength > 

     DEFAULT_MIN_LENGTH_TO_PLAY) {
NSLog(@"start playback for %@", downloadFileName);
[self initAndStartStreamer];
}
}


The above code is the NSURLConnection delegate method for receiving the data and writing the data to the file. Once I have "enough" of the MP3 I can start playing it by create the streamer and passing it the URL to our audioFile.

NEW: Handling EOF

One bug that I ran into since my initial post is that if I actually put this on my phone and went outside, away from my wireless network, the song would sometimes end early.  What was happening is that the AudioStreamer was playing the file faster than it was being downloaded.  The AudioStreamer would hit the EOF and then stop.  To prevent this case I added some "throttling" to the playback loop that checks if we are too close to the end of the file and changes the state to buffering until we have enough to continue. The following code is in startInternal.

// Flag to indicate that we have gotten too close to the EOF
// before the entire file has downloaded so we need throttle
// the playback.
BOOL isThrottling = NO;{

//
// Process the run loop until playback is finished or failed.
//
BOOL isRunning = YES;
do
{
// If we are playing a fixed-length MP3 make sure we are 

// not too close to the end of the file. This prevents us
// from hitting the end of the file before it is fully 
// downloaded. Very useful when not on 3G since the song 
// may be played faster than it is downloaded.
if (!fixedLength || self.fileDownloadComplete ||
self.fileDownloadCurrentSize > (fileDownloadBytesRead +
(kAQBufSize * kNumAQBufs))) {

isRunning = [[NSRunLoop currentRunLoop
runMode:NSDefaultRunLoopMode
beforeDate:

  [NSDate dateWithTimeIntervalSinceNow:0.25]];

if (isThrottling) {
isThrottling = NO;
AudioQueueStart(audioQueue, NULL);
self.state = AS_PLAYING;
}

//
// If there are no queued buffers, we need to check here 

// since the handleBufferCompleteForQueue:buffer: should 
// not change the state(may not enter the synchronized 
// section).
//
if (buffersUsed == 0 && self.state == AS_PLAYING)
{
self.state = AS_BUFFERING;
}
} else if (!isThrottling && self.state == AS_PLAYING) {
NSLog(@"Throttle because we are too close to EOF.");

self.state = AS_BUFFERING;

AudioQueuePause(audioQueue);
isThrottling = YES;

// Sleep for a few seconds
[NSThread sleepForTimeInterval:3.0];
}

} while (isRunning && ![self runLoopShouldExit]);


Seeking?

I was unable to figure out a way to allow for seeking. This would also provide a way for playback to pick up where it left off when the connection during the download phase.  If anyone has ideas on how to support this I would love to hear about it.

Conclusion

This is still far from ideal and it really seems like this is something that should be a lot easier to do within the SDK.  In the meantime, hopefully this is helpful to others.

You can download my source code here.

Tuesday, February 24, 2009

Getting the errorID from an ErrorEvent in ActionScript

Our Episodic Video Player is written in ActionScript 3. I'm still fairly new to ActionScript but came up with a solution to a problem the other day that I thought I should share.

The player loads various files from the server. In some cases I want to display an error when one of those files can't be found. I have an error handler that listens for Error Events and displays an appropriate error message depending on the type of the error event.  For example, I know that error code 2032 will generally mean in our case that the file could not be found. 

Alright, so this all seems simple enough except for one problem.  When I looked at the ASDocs for ErrorEvent.errorID I noticed that that property is not available for Flash Player 9.  Fortunately, ErrorEvent extends TextEvent which has text property I can access.  In the case of an ErrorEvent the text property contains a message that includes the error ID. Therefore, I just wrote my own getter to grab the error ID.

public function get errorID():int {
if ((text != null) && (text.indexOf("Error #") == 0)
&& (text.length > 7)) {
// The Error IDs always seem to be 4 digits
return parseInt(errorEvent.text.substr(7, errorEvent.text.length - 7));
}

Tuesday, February 17, 2009

Override attribute accessor and before_type_cast

Our Episodic Video Publishing Web Application is written in Ruby on Rails. I recently came across a situation where I had a model where I wanted to override it's attribute accessors in some cases. Consider a model that looks like this.

create_table "podcasts", :force => true do |t|
t.string "name"
t.boolean "use_show_name", :default => true
t.integer "show_id"
end


As you can see above that each podcast belongs to a show. I also have a form for this model and in the name field I want to display the name of the podcast unless "use_show_name" is true then I want to display the name of the show that this podcast belongs to.

<%= text_field :podcast, :name, :class => "text", :disabled => @podcast.use_show_name %>

I don't want to put this logic in the ERB template but instead I want to put it in the model so that anyone that calls Podcast.name will get the appropriate value depending on the "use_show_name".

class Podcast < ActiveRecord::Base
belongs_to :show
 
def name
return self.use_show_name ? self.show.name : read_attribute(:name)
end
alias_method :name_before_type_cast, :name
end


So there a few things going on above.

  • We override the "name" method by simply implementing it. This works because ActiveRecord using method_missing to implement it's "name" getter but since there is now a "name" method, method_missing will not be invoked.

  • In the method we check "use_show_name" to see what value to return.  If it is true we get the value from the show object.  Otherwise, we call read_attribute to get the value of the name attribute that stored in the DB.

  • Lastly, we alias the name method as "name_before_type_cast" since this is the method that is called by FormHelper.text_field.  ActiveRecord form helpers call the before_type_cast so that they can get the string value of the attribute but we want it to get the value from our "name" method.

Friday, January 2, 2009

Prompt User About Unsaved Changes Using Mootools

Mootools has a very useful method toQueryString on the Form element class.  In short, this method takes all the form fields and builds a query string generally used when sending form data to the server.  However, I found another use for this method that I thought I would share.

For our Episodic Video Publishing Web Application we have a number of pages that include a form.  If the user tries to navigate from a form that has unsaved changes then we want to prompt the user that they have unsaved changes and that these changes will be lost if they navigate away.  There are various ways that you can detect if the user has modified the page's form but the easiest I've found is to make use of Form.toQueryString.

First, create a function that stores the form state in a hidden input.

getOrCreateInitialFormState: function(form) {
var formInitialState = null;,
var formInitialState = $('form_initial_state');
if (!formInitialState)
formInitialState = new Element('input', {
id: 'form_initial_state',
type: 'hidden',
value: form.toQueryString()
}).inject(form, 'before');
return formInitialState;
}


So when the page loads record the current form state and hook into the click event for any links on your page and check if the form state has changed.

window.addEvent('domready', function()
{
// Grab the form
var form = $('my_form');

// Store the form state in a hidden input
getOrCreateInitialFormState(form);

// Capture any clicks that would cause the user to navigate away
$$('a').addEvent('click', function(e) {
if (form.toQueryString() != getOrCreateInitialFormState(form) {
e.stop();
var result = confirm('You have unsaved changes.' +
' Are you sure you want to navigate away from this page?');
if (result == true) {
// The user wants to navigate
window.location.href = a.get('href');
}
}
});
});


That's it!