Share Media on Twitter Using Flex, Part III: Video

This is the third and final part in our Share Media on Twitter Using Flex series (read Part I and Part II). In the first article we showed you how to build a Twitter application with the Flex framework in Flash Builder, importing Photoshop artwork into Flash Catalyst to build both the interface and its interactions. The second article took you through the processes and libraries needed to upload images to Flickr and use the bit.ly API to shorten the links to those images. With this final article in the series we’ll extend the application to upload video to Flickr, and use a few built-in Flex features to grab screenshots from the video and upload them as well.

Once you’re done, be sure to test your knowledge in our Article Quiz!

In the previous installment we discussed potential ways of hosting images online to accompany a Tweet. We decided the simplest approach was to upload them to Flickr, create a minimized link to the Flickr page using the bit.ly API, and then include that link in a tweet. That approach worked well, so we’ll do the same for video.

Before we start, you should download the code archive to follow along with. The file we’ll be working with this time around is cheepcheep_video_flashbuilder.fxp, in the Flex projects folder (the previous versions of the application are there as well, so you can see what’s changed). Import that file into Flash Builder, and you’re ready to get started.

Uploading Video to Flickr

Our first task is to modify the filter used by the FileReference element to include the video file extensions supported by Flickr. We’ve added avi, wmv, mov, mpg, mp4, m4v, and 3gp to the FileFilter that we created in the last article. Although these extensions are all officially supported by Flickr, there is still a risk that Flickr will be unable to encode some files; this is because video can be compressed with a variety of different codecs and only the most commonly used (like H.264) are supported. We recommend that you check the Flickr documentation for what’s supported.

Flickr allows you to upload video of up to 90 seconds in duration and 150MB in size for free accounts, or up to 500MB for Pro accounts. Realizing that video files can be larger than images, we’ve tweaked the original interface, adding an extra Label component to display the size of the selected file. We’ve also added a button to view video options if a video file is selected. We’ll look at this button in more detail later; for now just notice that it’s disabled by default.

The fileAccessed function, called once a file has been selected, has been modified to pull in extra information: the size of the file (which we display with our new Label), and its extension. FileReference does have a type property, but it’s unreliable (for example, it’s unable to determine the extension of a GIF file), so we’re using our own variable. To obtain the extension, we split the filename at each period and grab the last element of the resulting array. We’ve also added arrays at the top of the main application to store the acceptable extensions for both photo and video files:

private var photoExtensions:Array = ["gif","jpeg","jpg","png"]; 
private var videoExtensions:Array = ["avi","wmv","mov","mpg","mp4","m4v","3gp"];

...

private function fileAccessed(evt:Event):void {
 photoFileSize.text = (Number(fileReference.size)/100) + "kb";
 photoFileName.text = fileReference.name;
 var filename:Array = fileReference.name.split(".");
 flickrUploadType = filename[filename.length - 1];
 photoUploadBtn.enabled = true;
}

A successful upload to Flickr will call the uploadCompleteHandler function. We’ve modified that function to assign the uploaded media’s ID to a new flickrUploadID variable. We also test the extension of the file to see if it’s in the array of accepted video extensions. If it is, we enable the new Video Options button. The rest of this function, which grabs the URL of the upload from Flickr and submits it to our bit.ly service, is unchanged from the last article:

private function uploadCompleteHandler(evt:DataEvent):void { 
 CursorManager.removeBusyCursor();
 var xData:XML = new XML(evt.data);
 flickrUploadID = xData.photoid;  
 if ( videoExtensions.indexOf(flickrUploadType) != -1 ) {
   videoOptionsBtn.enabled = true;
 }
 photoUrl = "http://www.flickr.com/photos/"+flickrNsid+"/"+xData.photoid;
 bitlyService.send();
}

Fetching the Video from Flickr

We’re going to use the Video Options button to switch to a new state (videoOptions) that we’ve added to the application. This state displays a new component that we created with Flash Catalyst: its purpose is to display the uploaded video to enable the user to grab a snapshot of the video as an image for uploading as well. Before switching to this state, however, we need to retrieve the video from Flickr for playback. So we’ll write one function to send a request to Flickr when the button is clicked, and another to switch the application’s state when a response is received.

The first function, which we’ve called showVideoOptions, calls the getSizes method of the Flickr API using the same Flickr ActionScript library employed in the last article. It also sets an event listener to handle the API response:

private function showVideoOptions():void {  
 flickr = new FlickrService(flickrApiKey);  
 flickr.addEventListener(FlickrResultEvent.PHOTOS_GET_SIZES, handleVideoData);  
 flickr.photos.getSizes(flickrUploadID);  
}

Now let’s take a look at our callback, the handleVideoData function. If the upload is still being processed by Flickr, the FlickrResultEvent that gets passed to this function will contain an error to notify us that the video hasn’t been found. On the other hand, if the video has been processed, the result will contain an array of photo sizes. One of the “photo sizes” is actually the MP4 video.

Our first task then is to see if the data received contains an error. We can do this using the built-in function hasOwnProperty. If we find an error, we display an Alert to the user to inform them that the video is still processing. Otherwise, we switch to the videoOptions state to display the new component, then loop over the photoSizes array looking for a value of “Site MP4″ (which is the “size” string that Flickr assigns to MP4 video). We then pass the source property of that array index to our custom component’s videoLocation property, and call the component’s setVideoPlayer method:

private function handleVideoData(evt:FlickrResultEvent):void {  
 if ( evt.data.hasOwnProperty("error") ) {  
   Alert.show("It appears that Flickr is still processing your video,  
   please wait another minute and try again", "Video not available");  
 } else {  
   currentState = "videoOptions";  
   var len:Number = evt.data.photoSizes.length;  
   for (var i:Number = 0;i<len;i++) {  
     if ( evt.data.photoSizes[i].label == "Site MP4" ) {  
       customcomponent31.videoLocation = evt.data.photoSizes[i].source;  
       customcomponent31.setVideoPlayer();  
       break;  
     }  
   }  
 }  
}

Displaying the Video

CustomComponent3 has a VideoElement component positioned in the component's layout using a Group. VideoElement is a new component introduced with Flex 4: it's basically just a chrome-less version of the VideoPlayer component. It's really handy for grabbing video frames, as there's no need to strip away the player controls. The Group element is necessary for reasons I'll explain shortly.

The setVideoPlayer function, which we called from handleVideoData, assigns the video's URL to the source property of the VideoElement component. This will make the component download and play the video. We're also using a ProgressBar component, set to indeterminate mode, to give the user a visual cue that the file is being downloaded. To hide the progress bar once the download is complete, we need to add a new event listener to the component's init function. The listener calls the videoLoadProgress function, which hides the progress bar and enables the snapshot:

private function init():void {  
 ...  
 videoPlayer.addEventListener(ProgressEvent.PROGRESS,videoLoadProgress);  
 ...  
}  
 
...  
 
private function videoLoadProgress(evt:ProgressEvent):void {  
 if ( evt.bytesLoaded == evt.bytesTotal || videoPlayer.playing ) {  
   videoProgress.visible = false;  
   snapshotBtn.enabled = true;  
 }  
}

We need one more event handler, to enable the Replay button once the video is done playing. We'll add another listener to the init function (using the videoElement.COMPLETE event) and create a method called enableReplayBtn, which simply sets the value of the button's enabled property to true. It might seem like it would be easier just to add an inline listener to the videoElement itself, since our enableReplayBtn function is only one line of code. However, in the interest of keeping our code readable and maintainable, it makes more sense to have all our listeners in the same place.

Taking Snapshots

The Snapshot button calls the frameGrab function, which I've listed below. In order to take a snapshot of the video, we're using a function of Flex's built-in ImageSnapshot helper class. Rather than actually grabbing a frame directly from the video, captureImage just returns the current image displayed by the element you point it at. This is why we used VideoElement instead of VideoPlayer: we'd prefer to avoid grabbing the player controls along with the screenshot.

The ImageSnapshot class can capture from any component that implements the flash.display.IBitmapDrawable class. This includes Flex UIComponents, but doesn't include VideoPlayer or VideoElement; this is why we wrapped the VideoElement in a Group.

We assign the data grabbed by the captureImage function to a variable (so we can access it later from the main application), typecasting it as a ByteArray using the as keyword. Then we assign that variable to the source attribute of a new Image component to display a preview of the snapshot. Finally, we enable the Upload button:

private function frameGrab(evt:Event):void {  
 var imageSnap:ImageSnapshot = ImageSnapshot.captureImage(videoGroup);  
 snapshotPreview.source = imageSnap.data as ByteArray;  
 uploadSnapshotBtn.enabled = true;  
}

With this code in place, users can view the video, replay it as often as they like, and take snapshots of it while it's playing. Next, we'll add functionality to allow them to upload the current snapshot to Flickr.

Uploading a ByteArray as Image

The Upload class of the Flickr library that we're using was designed to receive a file for upload from the user's file system. For our photo uploads, we've used the FileReference element to do this. However, rather than being a file, the snapshot that we're capturing is a value that exists in memory and is inaccessible to the FileReference element. So how do we upload it?

There's no easy solution, but fortunately for us another person has encountered this issue and has modified the Upload class in the Flickr library to add the required functionality (http://blog.dannypatterson.com/?p=250). We've copied the uploadBytes method from that blog post and added it to the Upload.as file (located in com.adobe.webapis.flickr.methodgroups). This function allows us to upload our ByteArray as if it were an image. We need to modify the function slightly, adding an event listener to fire when the upload is complete. This listener calls another new function that we've also added to Upload.as: uploadBytesComplete. We use it to dispatch the event so that we can catch it within our application once the image has finished uploading:

public function uploadBytes(...) : Boolean {   
 ...  
 loader.addEventListener(Event.COMPLETE, uploadBytesComplete);  
 ...  
}  
 
private function uploadBytesComplete(event:Event):void {  
 var result:FlickrResultEvent = new FlickrResultEvent(PHOTOS_UPLOAD_BYTES_COMPLETE );  
 MethodGroupHelper.processAndDispatch( _service, URLLoader( event.target.loader ).data, result,"photoid", MethodGroupHelper.parseUploadBytesResult );      
}

The uploadBytes method behaves in almost exactly the same way as the Flickr upload method. However, a few more parameters need to be passed to it: an image title, a description, a list of tags, and a "public" flag. These parameters are also available to upload, but they're optional and we omitted them for the sake of brevity.

To make use of this added functionality, we've written a new function in our main application file: uploadVideoCap. This function is very similar to uploadFlickr, except that we're using uploadBytes instead of upload. You'll notice that we're setting flickr.permission to "write." This is required for the uploadBytes function to work correctly:

public function uploadVideoCap(capData:ByteArray):void {   
 flickr = new FlickrService(flickrApiKey);  
 flickr.secret = flickrSecret;  
 flickr.token = flickrAuthToken;    
 flickr.permission = "write";    
 
 flickr.addEventListener(  
   FlickrResultEvent.PHOTOS_UPLOAD_BYTES_COMPLETE,  
   videoCapUploaded    
 );  
   
 var uploader:Upload = new Upload(flickr);  
 uploader.uploadBytes(capData, "Video snapshot", "From Flex", "twitter,test,video,snapshot", true);  
 CursorManager.setBusyCursor();          
}

When the screenshot is done uploading we call the videoCapUploaded function, which just grabs the Flickr URL and submits it to the bit.ly service we created in the last article:

private function videoCapUploaded(evt:FlickrResultEvent):void {   
 CursorManager.removeBusyCursor();  
 photoUrl = "http://www.flickr.com/photos/"+flickrNsid+"/"+evt.data.photoid;  
 bitlyService.send();          
}

Back in our custom component, we've created a function called uploadSnapshot to call uploadVideoCap. uploadSnapshot is then bound to the Upload button with an event listener in the component's init function. To call the uploadVideoCap function from inside the component, we need to specify its path using mx.core.FlexGlobals.topLevelApplication:
 
private function uploadSnapshot(evt:Event):void {  
 mx.core.FlexGlobals.topLevelApplication.uploadVideoCap(imageByteArray);  
 closeState();  
}

We’re almost done! Now all we need is a method to close the videoOptions state, to make sure we stop the video playback and switch the application back to the main twitterDisplay state. The closeState method itself is fairly self-explanatory. Notice that it has an optional evt:Event parameter. This is because we’ll also be calling the method as an event handler for the component’s close button (in which case it will implicitly be passed an Event parameter). This event handler is declared in the component’s init function:

private function init():void {  
 ...  
 closeBtn.addEventListener(MouseEvent.CLICK,closeState);  
 ...  
}  
 
...  
 
private function closeState(evt:Event = null):void {  
 videoPlayer.stop();  
 mx.core.FlexGlobals.topLevelApplication.currentState = "twitterDisplay";      
}  

And that's it! This has been an interesting application to put together. While it's certainly functional, there are a lot of features that could be added to round it off: for example, you could limit the Twitter text input to 140 characters, or refine the video snapshot interaction. The ImageSnapshot functionality is so simple that it's easy to imagine using it in other video applications: adding annotations for feedback to the camera operator or editor, for example.

Hopefully this series has shown you how easy it is to design and build a rich Internet application using Flash Catalyst and Flash Builder. You should also have a feel for how to interact with a few major web APIs, which will come in very handy in developing your own social media applications!

So what are you waiting for? Get out there and build something cool! But before you do, why don't you test what you've learned by taking our Article Quiz?

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.