Creating A Downloader For YouTube with Flex/Air

by Jack Herrington 26

YouTube’s mixed easy movie access with community uploads to create a startling new service. The online problem is that you can only access YouTube when you are online. How can you access those movies when you are offline? Let’s solve that problem by building a downloader with Flex and AIR.
In this article we will build a cross platform application that searches for YouTube videos and then provides a mechanism to download those videos and view them locally. You will be able to take your favorite YouTube videos with you wherever you go.

Requirements

Flex 3

Try/Buy

Sample files:

YouTube.zip
Let’s start by building the search user interface.

Searching YouTube

YouTube provides a set of RSS feeds that keep you up to date with the latest videos. These feeds take lots of parameters to refine what you are looking for, one of those is an arbitrary keyword search.
The Flex code below uses the YouTube search feed to request a set of videos based on the user’s search criteria. The code then uses the e4x language extensions in ActionScript 3 to parse the feed and present the video thumbnails in a TileList.

<?xml version="1.0" encoding="utf-8"?> 

  <mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"  title="YouTube  Search"> 

  <mx:Script> 

  <![CDATA[ 

  import
 mx.rpc.events.ResultEvent; 

  import
 mx.rpc.http.HTTPService; 

namespace
 atom = "http://www.w3.org/2005/Atom"
; 

    namespace
 media = "http://search.yahoo.com/mrss/"
; 

private
 function
 onSearch() : void
 { 

    var
 search:HTTPService = new
 HTTPService(); 

    search.url = "http://gdata.youtube.com/feeds/api/videos/?vq="
+escape(txtSearch.text)+"&orderby=updated"
; 

    search.resultFormat = 'e4x'
; 

     search.addEventListener(ResultEvent.RESULT,onSearchResult); 

    search.send(); 

  } 

  private
 function
 onSearchResult( event:ResultEvent ) : void
 { 

    use
 namespace
 atom; 

    use
 namespace
 media; 

    var
 movies:Array = []; 

    for
 each
( var
 entry:XML in
 event.result..entry ) { 

      var
 group:XML = entry.group[0]; 

      movies.push( { 

         id:[email protected](), 

         description:entry.content.toString(), 

         thumbnail:group.thumbnail[0][email protected]() } ); 

    } 

    thumbList.dataProvider =  movies; 

  } 

  ]]> 

  </mx:Script> 

  <mx:HBox  width="100%"> 

  <mx:TextInput  width="100%" id="txtSearch" /> 

  <mx:Button  label="Search" click="onSearch()" /> 

  </mx:HBox> 

  <mx:TileList  id="thumbList" width="100%" height="100%"> 

  <mx:itemRenderer> 

  <mx:Component> 

  <mx:HBox  paddingBottom="5" paddingLeft="5" paddingRight="5"  paddingTop="5"> 

  <mx:Image  source ="{data.thumbnail}" height="100" width="150" horizontalAlign="center" verticalAlign="middle" toolTip="{data.description}"> 

  </mx:Image> 

  </mx:HBox> 

  </mx:Component> 

  </mx:itemRenderer> 

  </mx:TileList> 

  </mx:WindowedApplication>

The onSearch method is called from the search button. It creates a new HTTPService on the fly with the URL of the YouTube feed for the search. It then registers onSearchResult as an event handler for the result.
The onSearchResult method uses e4x to parse through each ‘entry’ tag in the RSS feed. For each movie it builds an object with three fields. The ‘id’ field holds the URL of the HTML page for the video. The ‘description’ field holds the textual description of the video. And the ‘thumbnail’ field holds the URL of the thumbnail.
When I launch this MXML in an AIR project within Flex Builder 3 I see something like Figure 1.

Figure 1-1. Just the YouTube search

Figure 1-1. Just the YouTube search

In this case I’ve typed in ‘super mario’ and pressed ‘search’ to get a list of the movies that matched that criteria.
From here we need to add the ability to download the flash video, as well as play it back.

Downloading From YouTube

The user interface for the download version of the project is going to be a lot more complex than the search interface. We need to show the search results, allow for playback, and add a Save button to save the movie locally. The finished product is shown in Figure 2.

Figure 1-2. The search and the downloader

The interface is separated into two pieces, defined by panels. One panel is for searching and the other panel is shows the downloaded movies. The vertical divider allows you to scale the size of each of these panels to your preferred size.
The search section is largely the same though we will add a progress indicator (invisible in the figure) next to the search button. That progress indicator will be used during the downloads of the movies since those tend to be fairly big files.
The downloaded movies panel has the list of downloaded movies on the left, and the movie player on the right. As well as a Save button that is only visible if a movie is selected. The Save button allows the user to copy the downloaded movie out of the temporary directory onto the desktop (or wherever they choose).
The user interface portion of the MXML code for this example is shown below:

<?xml version="1.0" encoding="utf-8"?> 

  <mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"  title="YouTube  Downloader" 

    height="700" width="600" paddingBottom="5" paddingLeft="5"  paddingRight="5" paddingTop="5" 

     creationComplete="updateLocalVideos()"> 

  <mx:VDividedBox width="100%" height="100%"> 

<mx:Panel  title="Search" paddingBottom="5" paddingLeft="5"  paddingRight="5" paddingTop="5" width="100%"  height="50%"> 

  <mx:HBox  width="100%"> 

  <mx:TextInput  width="100%" id="txtSearch" /> 

  <mx:Button  label="Search" click="onSearch()" /> 

  <mx:ProgressBar width="100" id="downloadProgress" visible="false" /> 

  </mx:HBox> 

  <mx:TileList  id="thumbList" width="100%" height="100%" doubleClickEnabled="true" doubleClick="onThumbClick()"> 

   ... 

  </mx:TileList> 

  </mx:Panel> 

<mx:Panel  title="Downloaded  Movies"  paddingBottom="5" paddingLeft="5" paddingRight="5"  paddingTop="5" width="100%" height="50%"> 

  <mx:HBox  height="100%"> 

  <mx:List  id="localList" width="170" height="100%" doubleClickEnabled="true" doubleClick="onMovieClick()"  > 

   <mx:itemRenderer> 

   <mx:Component> 

   <mx:HBox paddingBottom="2" paddingLeft="2"  paddingRight="2" paddingTop="2"> 

   <mx:Image source="{data.thumbnail.url}" height="100"  width="140" horizontalAlign="center" verticalAlign="middle"> 

   </mx:Image> 

   </mx:HBox> 

   </mx:Component> 

   </mx:itemRenderer> 

  </mx:List> 

  <mx:VBox  verticalAlign="middle" horizontalAlign="center" width="100%" height="100%"> 

  <mx:VideoDisplay id="player" width="300" height="200" backgroundColor="black" /> 

  <mx:Button  id="btnSave" label="Save" visible="false" click="onSave()" /> 

  </mx:VBox> 

  </mx:HBox> 

  </mx:Panel> 

  </mx:VDividedBox>

To keep the code sample short I’ve omit a bit of the original code from the display of the movies found during the search.
Don’t be put off by the quantity of tags. The MXML is quite straightforward. Most of the tags define the ‘itemRenderer’s used by the list control to show the video thumbnails. The other elements, the panels, the video display, the save button and so on, are just single elements with a few attributes to refine them.
With the interface all laid out it’s time to dig into the code. The search code remains exactly the same, but we’ve now added a thumbClick event handler which is called when the user double clicks on a thumbnail in the search panel. The thumbClick handler starts the request for the HTML page associated with the YouTube video. The onHTMLComplete method receives the HTML code for the page as text. It then calls getFLVURL to get the URL for the FLV data for the movie.
The code for this is shown below:

<mx:Script> 

  <![CDATA[ 

  import
 mx.rpc.events.ResultEvent; 

  import
 mx.rpc.http.HTTPService; 

  import
 com.adobe.serialization.json.JSONDecoder; 

namespace
 atom = "http://www.w3.org/2005/Atom"
; 

    namespace
 media = "http://search.yahoo.com/mrss/"
; 

private
 function
 onSearch() : void
 { ...} 

    private
 function
 onSearchResult( event:ResultEvent ) : void
 { ... } 

public
 function
 getFLVURL( sHTML:String ) : String { 

    var
 swfArgsFound:Array = sHTML.match( /var swfArgs =(.*?);/
 ); 

    var
 swfArgsJS:JSONDecoder = new
 JSONDecoder( swfArgsFound[1] ); 

    var
 swfArgs:Object = swfArgsJS.getValue(); 

    var
 url:String = 'http://youtube.com/get_video.php'
; 

    var
 first:Boolean = true
; 

    for
( var
 k:String in
 swfArgs ) { 

      if
 ( swfArgs[k] != null
 && swfArgs[k].toString().length >  0 ) { 

        url += first ? '?'
 : '&'
; 

        first = false
; 

        url += k+'='
+escape(swfArgs[k]);    

      } 

     } 

    return
 url; 

  } 

private
 function
 onHTMLComplete( movie:Object, event:Event )  : void
 { 

    var
 loader:URLLoader = event.target as
 URLLoader; 

    var
 movieID:String = movie.id.split( /=/ )[1]; 

    var
 flvStream:URLStream = startRequest( movieID+'.flv'
,  getFLVURL( loader.data ) ); 

    startRequest( movieID+'.jpg'
,  movie.thumbnail ); 

   flvStream.addEventListener(Event.COMPLETE,function
(event:Event):void
 { 

      downloadProgress.visible = false
; 

      updateLocalVideos(); 

    } ); 

    downloadProgress.source =  flvStream; 

    downloadProgress.visible = true
; 

  } 

private
 function
 onThumbClick() : void
 { 

    var
 htmlLoader:URLLoader = new
 URLLoader(); 

     htmlLoader.addEventListener(Event.COMPLETE, 

      function
( event:Event ) : void
 { onHTMLComplete( thumbList.selectedItem,  event ); } ); 

    htmlLoader.load(new
 URLRequest(thumbList.selectedItem.id)); 

  }

Let me step back for a second and talk briefly about Flash Video. YouTube, like any other site that uses a Flash player to show videos, uses FLV as the movie format. What YouTube does is provide their Flash video application with enough data for it to construct a ‘source’ URL for it’s video display object. That ‘source’ URL is get_video.php along with a set of parameters. Those parameters are stored in a Javascript variable called ‘swfVars’ which is embedded in the page.
The GetFLVURL takes the Javascript from the page and extracts the ‘swfVars’. It then uses the JSONDecode class provided by the as3corelib (http://code.google.com/p/as3corelib/) to decode the Javascript into an ActionScript object. From there it construct the FLV URL from the parameters in the ActionScript object.
The weakness in this example application is that it uses this ‘screen scraping’ technique to get to the FLV URL. Unfortunately there is no easier way to do it. If the format of the YouTube HTML changes, then this code might need to be rewritten to compensate for the changes.
Once the FLV URL is constructed the onHTMLComplete method calls startRequest on both the thumbnail and the FLV to download the data and store it locally. The code for this is shown below:

private
 function
 onReqComplete( fileName:String, event:Event  ) : void
 { 

    var
 stream:URLStream = event.target as
 URLStream; 

    var
 byteLength:int = stream.bytesAvailable; 

    var
 bytes:ByteArray = new
 ByteArray(); 

    stream.readBytes( bytes, 0, byteLength  ); 

    stream.close(); 

  if
 ( File.applicationStorageDirectory.exists == false
 ) 

        File.applicationStorageDirectory.createDirectory(); 

    var
 f:File = new
 File(  File.applicationStorageDirectory.nativePath + File.separator + fileName ); 

    var
 fs:FileStream = new
 FileStream(); 

    fs.open( f, FileMode.WRITE ); 

    fs.writeBytes( bytes, 0,  byteLength ); 

    fs.close(); 

  } 

  private
 function
 startRequest( fileName:String, url:String )  : URLStream { 

    var
 req:URLStream = new
 URLStream(); 

    req.addEventListener(Event.COMPLETE, function
 ( event:Event ) : void
 { onReqComplete( fileName, event ); } ); 

    req.load( new
 URLRequest( url ) ); 

    return
 req; 

  }

The startRequest builds a URLStream object to get the data for the given URL. It then sets up onReqComplete as an event handler for when the download is complete. The onReqComplete uses the AIR File API to store the data, which is read directly into an AIR ByteArray class, into a file stored in the application storage directory. The application storage directory is maintained by AIR automatically for you.
Once the files are downloaded the updateLocalVideos method is called. This method, shown below, updates the list of local videos in the downloaded videos panel.

private
 function
 updateLocalVideos() : void
 { 

    var
 fileNames:Object = new
 Object(); 

    for
 each
 ( var
 file:File in
 File.applicationStorageDirectory.getDirectoryListing() ) { 

      var
 fName:String = file.name.split( /[.]/
 )[0]; 

      fileNames[ fName ] = true
; 

    } 

    var
 movieList:Array = []; 

    for
( var
 fileKey:String in
 fileNames ) { 

      var
 thumb:File = new
 File(  File.applicationStorageDirectory.nativePath + File.separator + fileKey + '.jpg'
 ); 

      var
 movie:File = new
 File(  File.applicationStorageDirectory.nativePath + File.separator + fileKey + '.flv'
 ); 

      if
 ( thumb.exists && movie.exists ) 

        movieList.push( {  thumbnail: thumb, movie: movie } ); 

    } 

    localList.dataProvider =  movieList; 

  }

To do that the method uses the getDirectoryListing method, provided by the AIR File API, to get all of the files in the application storage directory. It then chops off the extensions and creates a list of just the file names. From there builds the list of local movies by iterating through the file names and checking to make sure that both the ‘.flv’ file for the video, and the ‘.jpg’ file for the thumbnail, are available.
With the list of local videos in hand the method sets the dataProvider of the local list to the movie list that it generated.
From there the final few functions handle the playback and the saving of the FLV to the desktop. These methods are shown below:

private
 function
 onMovieClick() : void
 { 

    player.source =  localList.selectedItem.movie.url; 

    btnSave.visible = true
; 

  } 

  private
 function
 onSave() : void
 { 

    var
 f:File = File.desktopDirectory; 

    f.addEventListener(Event.SELECT,onSaveSelect); 

    f.browseForSave("Save FLV"
); 

  } 

  private
 function
 onSaveSelect( event:Event ) : void
 { 

    var
 f:File = event.target as
 File; 

    var
 lf:File = localList.selectedItem.movie as
 File; 

    lf.copyTo( f, true
 ); 

  } 

  ]]> 

  </mx:Script> 

</mx:WindowedApplication>

The onMovieClick method is called when the user double clicks on a movie in the local list. It just sets the source of the VideoDisplay to the url of the local ‘.flv’ file.
The onSave method is called when the user clicks the Save button. It pops up a Save dialog using the AIR File API. If the user hits Save then the onSaveSelect method is called which copies the ‘.flv’ file from the local storage directory to the location they specify. You can see the interface for this in action in Figure 3.

Figure 1-3. The save window

On it’s face it seems like a lot of code, but it’s really not all that daunting when you break it down into it’s component pieces.

Your Next Steps

I hope you can leverage the code that I have presented in this article in your own work. There are some good reusable fragments including the file request and storage code in on ReqComplete. The JSON parsing in GetFLVURL can also come in handy when dealing with websites that lack a web services interface. Or in this case, have a web service interface that lacks the information you require.
If you do make use of this code, be sure to let me know. You can contact me directly through my website; http://jackherrington.com.

Jack Herrington

Jack Herrington is an engineer, author and presenter who lives and works in the Bay Area. His mission is to expose his fellow engineers to new technologies. That covers a broad spectrum, from demonstrating programs that write other programs in the book Code Generation in Action. Providing techniques for building customer centered web sites in PHP Hacks and Getting Started With Flex 3. All the way writing a how-to on audio blogging called Podcasting Hacks. All of which make great holiday gifts and are available online here, and at your local bookstore. Jack also writes articles for O’Reilly, DevX and IBM Developerworks.

Jack lives with his wife, daughter and two adopted dogs. When he is not writing software, books or articles you can find him on his bike, running or in the pool training for triathlons. You can keep up with Jack's work and his writing at http://jackherrington.com.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>