Thursday, January 21, 2010

subtleties

Whew! I just finished a hell of a debugging session. My goal was to make Flash submit a url to tinyurl.com for minification.

My code worked locally (when I ran the swf on my desktop), but it didn't work in the browser. So I was pretty sure it was a security issue. But I couldn't understand why I wasn't getting a response from http://tinyurl.com, because they DO have a crossdomain.xml file. See: http://www.tinyurl.com/crossdomain.xml

hint: if you're smarter than I am, you may be able to guess the problem just via the previous paragraph.

Here's the code that didn't work:


send();

function send() : void
{
var url : String = "http://www.google.com/search?q=cows";
var urlLoader : URLLoader = new URLLoader();
urlLoader.addEventListener( Event.COMPLETE, receive );
urlLoader.load( new URLRequest( "http://www.tinyurl.com/api-create.php?url=" + url ) );
}

//this callback function never gets called!
function receive( event : Event ) : void
{
var urlLoader : URLLoader = event.target as URLLoader;
//output is a text field on the stage
output.text = urlLoader.data;
}


Here's the working version. Can you spot the difference?


send();

function send() : void
{
var url : String = "http://www.google.com/search?q=cows";
var urlLoader : URLLoader = new URLLoader();
urlLoader.addEventListener( Event.COMPLETE, receive );
urlLoader.load( new URLRequest( "http://tinyurl.com/api-create.php?url=" + url ) );
}

function receive( event : Event ) : void
{
var urlLoader : URLLoader = event.target as URLLoader;
//output is a text field on the stage
output.text = urlLoader.data;
}


Give up? The difference is in this statement:

BUGGY: urlLoader.load( new URLRequest( "http://www.tinyurl.com/api-create.php?url=" + url ) );

WORKING: urlLoader.load( new URLRequest( "http://tinyurl.com/api-create.php?url=" + url ) );

In case you still don't see it, the buggy version has a www in-front-of the url and the working version doesn't.

tinyurl's crossdomain.xml file is located at http://tinyurl.com/crossdomain.xml NOT http://www.tinyurl.com/crossdomain.xml.

But you'll get to it if you type it the "wrong" way into the browser, because most browser normalize urls. Flash doesn't. To Flash, there is no crossdomain.xml file, because it was specifically looking for one at the www address, and there isn't one there.

The scary thing is that my FIRST assumption was that it was a crossdomain.xml issue, so I watched the Activity Window in Safari as I ran my app (the bad version) in the browser. The Activity window showed that the crossdomain file was being accessed correctly. But apparently that was just the BROWSER accessing it correctly. Flash still thought of it as a bogus file, since it came from a www site.

Thursday, January 14, 2010

how long is an mp3?

I need to know the length (in seconds) of a progressive mp3 before it starts playing. As-far-as I can tell, there are only three ways of getting this information:

1) if it's explicitly stated in some sort of data feed (e.g. an XML playlist), you can, of course, grab that info and hope it's accurate.

2) you can read it from the id3 tags -- the metadata embedded in the mp3 file itself -- and hope THAT'S accurate.

3) you can guestimate length using some simple math.

Option one won't work for me, because my clients are generally not going to enter durations into a data feed.

I spent hours trying to get option two to work, emailing other developers and asking them for advice. I wasn't receiving the id3 data. Someone mentioned to me that I was likely hitting a security wall. It turns out that though a SWF can play mp3s from another domain, it can't access embedded id3 data without permission. See: http://kb2.adobe.com/cps/963/50c96388.html

I figured this was the problem, so I added a crossdomain file and a SoundLoaderContext object to Sound.load(), but I still didn't receive any id3 data. Why? Because there wasn't id3 data in the mp3s I was using -- even though the client assured me that there was.

I realized that I can't count on there being id3 data. Even if there is, there may not be length data, as it's optional which id3 tags are included in an mp3 file (or if any are included at all).

Which lead me to try option three. I patched it together based on various similar solutions I found online:

This code runs in a loop:


if( _duration == 0 && ( bytesLoaded / bytesTotal ) > 0.1)
{
_duration = _sound.length / 1000 / bytesLoaded * bytesTotal;
}


It waits until ten percent (0.1) of the mp3 is cashed and then calculates duration. It's an estimate, because it's based on Sound.length, which is NOT necessarily the length of the whole mp3. It's the length of what's loaded so far.

And you can't simply guess the length from bytesTotal, because depending on the compression/bitrate of the mp3, the same number of bytes could mean different lengths. So the calculation takes into account the known length, the number of bytes loaded so far and the total number of bytes. (Divided by 1000 to convert from milliseconds to seconds.)

How much should I cache before I let Flash do the calculation? Is ten percent enough? (The JW Player uses ten percent.) I tried it with an mp3 which I knew to be 1 minute and 2 seconds. The guestimation was close but not perfect: 00:59 and 1:00 on subsequent runs.

I upped the cached-percent to 0.2, which produced perfect guesses. Also, on my high-bandwidth (but not exceptionally high-bandwidth) connection, it took about a second for 0.2 percent to cache, which is acceptable for my purposes.

But then I tried a one-hour-long mp3. As you might expect, it took quite some time for 20 percent to cache. Totally unacceptable. I changed the percent back to 0.1 and the hour-long mp3 produced a guess in a reasonable amount of time (it took about fifteen seconds to guess). And the guess was only off by one second.

In the end, I am probably going to use a three-pronged approach: first, I will check for duration data in the XML feed, using that if it's there (doubtful). Next, I will try to use id3 data. Failing both those approaches, I will guestimate with a ten-percent cache.

If anyone reading this has a better idea, please let me know!

I am also trying to figure out how to adjust the UI if Flash has to guestimate. When the hour-long mp3 finished playing, the time-duration display said 00:59:00/1:00:00, which doesn't thrill me. Should I fudge the time up to the duration? (if end_time != duratation; then end_time = duration.)

Someone suggested that I keep checking for duration and adjust it as it the guess gets more accurate (which it will as more data gets cached.) I don't know. I think it would be pretty distracting seeing the the duration-readout change. Also, my played-progress bar would jump around, popping to a longer or shorter width.

What do you think?