Tuesday, September 1st, 2009
Update: I’ve added a nice little example below and all the code is up on Github.
So, I’ve been working on a simple tool that outlines how easy it is to upload a video to YouTube through your own web application.
Before you read this tutorial, it’s worth going through the YouTube GData API.
Now you may be asking yourself: “why would anyone need this?”, well think about this scenario:
You’re a brand owner and you’d like a gadget on your YouTube brand channel/iGoogle/microsite. You’d like the leverage the power of YouTube in terms of video serving as well as the community aspect. So you decide that you want users to upload their videos and maybe enter it into a contest.
Naturally, the easiest way is to get the user to go to YouTube and upload it there, but that’s not very intuitive and nor is it unobtrusive, so it’s much nicer to integrate it into your gadget!
Now, I’m going to be lazy here and just explain the bare bones, as in all you need to…
- Authenticate the user
- Send the initial meta data to YouTube about the video
- Upload the video to YouTube
- Handle the video id of the uploaded video
So where to begin? Well, since we can’t do everything in Flash (because we need iframes for the authentication and the upload process), I’ve split it into a set of simple code classes.
So let’s start with how we’re going to do this:
The first thing we need to do is create our HTML with the JavaScript and CSS we’re going to use to handle the iframes and forms for submission, so here’s the HTML to start with:
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<!--
/**
* @author Ahmed Nuaman (http://www.ahmednuaman.com)
* @langversion 1.1
*
* This work is licenced under the Creative Commons Attribution-Share Alike 2.0 UK: England & Wales License.
* To view a copy of this licence, visit http://creativecommons.org/licenses/by-sa/2.0/uk/ or send a letter
* to Creative Commons, 171 Second Street, Suite 300, San Francisco, California 94105, USA.
*/
-->
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
<!--
var flash;
function embedFlash()
{
$('#uploadDiv').hide();
$('#youtubeIframe').hide();
swfobject.embedSWF( 'App.swf', 'flash', '100%', '500', '9.0.0' , '', { }, { 'menu': 'false' }, { 'id': 'flashApp' } );
flash = document.getElementById( 'flashApp' );
}
function doLogin()
{
$('#youtubeAuthForm').submit();
$('#youtubeIframe').show();
}
function handleLogin(token)
{
$('#youtubeIframe').hide();
flash.handleLoginComplete( token );
}
function showUpload(url, token)
{
$('#uploadDiv').show();
$('#youtubeUploadForm').attr( 'action', url + '?nexturl=http://localhost:9852/upload-response.html' );
$('#youtubeUploadForm input[name=token]').val( token );
}
function doUpload()
{
$('#uploadDiv').hide();
$('#youtubeUploadForm').submit();
$('#youtubeIframe').show();
}
function handleUpload(videoId)
{
$('#youtubeIframe').hide();
flash.handleUploadComplete( videoId );
}
google.setOnLoadCallback( embedFlash );
google.load( 'jquery', '1' );
google.load( 'swfobject', '2' );
-->
</script>
<style type="text/css" media="screen">
<!--
html, body
{
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
.hide
{
display: none;
visibility: hidden;
}
#uploadDiv
{
position: absolute;
display: block;
top: 100px;
width: 100%;
text-align: center;
}
#youtubeIframe
{
position: absolute;
top: 0;
}
-->
</style>
<title>YouTube Auth Test
</title>
</head>
<body>
<div id="flash"></div>
<form id="youtubeAuthForm" action="https://www.google.com/accounts/AuthSubRequest" method="post" target="youtubeIframe" class="hide">
<input type="hidden" name="next" value="http://localhost:9852/auth-response.html" />
<input type="hidden" name="scope" value="http://gdata.youtube.com" />
<input type="hidden" name="session" value="0" />
<input type="hidden" name="secure" value="0" />
</form>
<div id="uploadDiv">
<form id="youtubeUploadForm" action="" method="post" enctype="multipart/form-data" target="youtubeIframe">
<input type="file" name="file" value="" />
<input type="hidden" name="token" value="" />
</form>
</div>
<iframe name="youtubeIframe" id="youtubeIframe" width="100%" height="500"></iframe>
</body>
</html>
So, the first thing I’m doing is loading in the jQuery and SWFObject libraries using the Google Load API. Once that’s all loaded, I then embed the flash, set a var in the document’s scope that’ll allow me to access the JavaScript API in the flash application and hide the iframe.
Now, let’s look at the flash, but don’t forget the HTML, as we will be back.
So here’s the MXML:
< ?xml version="1.0" encoding="utf-8"?>
<mx :Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="100%" height="100%" creationComplete="init()">
</mx><mx :Script>
< ![CDATA[
import com.firestartermedia.lib.as3.events.DataServiceEvent;
import com.firestartermedia.lib.as3.data.XMLDataService;
import mx.collections.ArrayCollection;
import mx.rpc.events.ResultEvent;
import mx.rpc.http.mxml.HTTPService;
[Bindable]
private var videoCategories:ArrayCollection;
private var browser:FileReference;
private var dataToken:String;
private var uploadToken:String;
private var uploadURL:String;
private function init():void
{
Security.loadPolicyFile( 'http://gdata.youtube.com/crossdomain.xml' );
loadVideoCategories();
}
private function loadVideoCategories():void
{
var service:XMLDataService = new XMLDataService();
service.addEventListener( DataServiceEvent.READY, handleVideoCategories );
service.send( 'http://gdata.youtube.com/schemas/2007/categories.cat' );
}
private function handleVideoCategories(e:DataServiceEvent):void
{
var data:XML = e.data as XML;
var categories:Array = [ ];
for each ( var category:XML in data..*::category )
{
categories.push( category.@term.toString() );
}
videoCategories = new ArrayCollection( categories );
}
private function handleLoginButtonClicked():void
{
ExternalInterface.addCallback( 'handleLoginComplete', handleLoginComplete );
ExternalInterface.addCallback( 'handleUploadComplete', handleUploadComplete );
ExternalInterface.call( 'doLogin' );
}
public function handleLoginComplete(token:String):void
{
dataToken = token;
stack.selectedIndex = 1;
}
private function sendVideoData():void
{
var service:HTTPService = new HTTPService();
service.contentType = 'application/atom+xml; charset=UTF-8';
service.headers[ 'Authorization' ] = 'AuthSub token="' + dataToken + '"';
service.headers[ 'GData-Version' ] = 2;
service.headers[ 'X-GData-Key' ] = 'key=ADD_YOUR_KEY_HERE';
service.method = 'POST';
service.request = '<?xml version="1.0"?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">'
+ '<media :group></media><media :title type="plain">' + videoTitle.text + '</media><media :description type="plain">' + videoDescription.text
+ '</media><media :category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">' + ComboBox( videoCategory ).selectedItem.toString()
+ '</media><media :keywords>' + videoKeywords.text + '</media></entry>';
service.resultFormat = 'e4x';
service.url = 'http://gdata.youtube.com/action/GetUploadToken';
service.addEventListener( ResultEvent.RESULT, handleVideoDataResult );
service.send();
}
private function handleVideoDataResult(e:ResultEvent):void
{
var data:XML = e.result as XML;
uploadToken = data..token;
uploadURL = data..url;
stack.selectedIndex = 2;
ExternalInterface.call( 'showUpload', uploadURL, uploadToken );
}
/* private function browseForFile():void
{
if ( !browser )
{
browser = new FileReference();
browser.addEventListener( Event.SELECT, handleFileSelected );
browser.addEventListener( ProgressEvent.PROGRESS, handleFileUploadProgress );
//browser.addEventListener( DataEvent.UPLOAD_COMPLETE_DATA, handleFileUploaded );
}
browser.browse();
videoUploadMessage.text = '';
}
private function handleFileSelected(e:Event):void
{
videoUploadMessage.text = browser.name;
videoUploadButton.visible = true;
}
private function handleFileUploadProgress(e:ProgressEvent):void
{
videoUploadProgress.maximum = e.bytesTotal;
videoUploadProgress.minimum = 0;
videoUploadProgress.setProgress( e.bytesLoaded, e.bytesTotal );
if ( e.bytesLoaded === e.bytesTotal )
{
handleFileUploaded();
}
}
private function handleFileUploaded(e:DataEvent=null):void
{
stack.selectedIndex = 4;
} */
private function uploadFile():void
{
/* var request:URLRequest = new URLRequest();
var vars:URLVariables = new URLVariables();
vars.token = uploadToken;
request.data = vars;
request.method = URLRequestMethod.POST;
request.url = uploadURL + '?nexturl=http://localhost:9852/upload-response.html';
browser.upload( request, 'file' ); */
ExternalInterface.call( 'doUpload' );
stack.selectedIndex = 3;
}
public function handleUploadComplete(videoId:String):void
{
videoUploadMessage.text = 'Your video has been upload and the id is: ' + videoId;
stack.selectedIndex = 4;
}
]]>
</mx>
<mx :Style source="styles.css" />
<mx :ViewStack id="stack" width="100%" height="100%" creationPolicy="all">
</mx><mx :Box width="100%" height="100%">
<mx :Button id="loginButton" label="Click here to log in to YouTube" click="handleLoginButtonClicked()" />
</mx>
<mx :Box width="100%" height="100%">
</mx><mx :Grid>
</mx><mx :GridRow>
</mx><mx :GridItem>
<mx :Text text="Title" />
</mx>
<mx :GridItem>
<mx :TextInput id="videoTitle" text="" />
</mx>
<mx :GridRow>
</mx><mx :GridItem>
<mx :Text text="Description" />
</mx>
<mx :GridItem>
<mx :TextInput id="videoDescription" text="" />
</mx>
<mx :GridRow>
</mx><mx :GridItem>
<mx :Text text="Keywords" />
</mx>
<mx :GridItem>
<mx :TextInput id="videoKeywords" text="" />
</mx>
<mx :GridRow>
</mx><mx :GridItem>
<mx :Text text="Category" />
</mx>
<mx :GridItem>
<mx :ComboBox id="videoCategory" dataProvider="{videoCategories}" />
</mx>
<mx :GridRow>
</mx><mx :GridItem>
</mx>
<mx :GridItem>
<mx :Button label="Submit" click="sendVideoData()" />
</mx>
<mx :Box width="100%" height="100%">
<!--<mx:Grid>
</mx><mx :GridRow>
</mx><mx :GridItem>
<mx :Button label="Browse for video..." id="videoUploadBrowseButton" click="browseForFile()" />
</mx>
<mx :GridItem>
<mx :Text id="videoUploadMessage" />
</mx>
-->
<mx :Button label="Upload this video" id="videoUploadButton" click="uploadFile()" />
<mx :Box width="100%" height="100%">
<mx :ProgressBar id="videoUploadProgress" width="300" indeterminate="true" />
</mx>
<mx :Box width="100%" height="100%">
<mx :Text id="videoUploadMessage" />
</mx>
Please ignore all the code problems in the MXML above, it’s the WordPress plugin not rendering it correctly. The code is better seen here: http://github.com/ahmednuaman/YouTube-Auth-And-Upload/blob/a6a7538a2eda39e207357925b74b48a0287387c5/src/App.mxml
Ok, so that’s quite a lot, so here’s a run down:
- First thing I do is load the categories that videos can be in, it’s a simple XML request and I use my “XMLDataService()“
- I then move to add the “ExternalInterface” callbacks, ready for JavaScript to interact with the flash application.
- Once the callbacks have been added, I fire a JavaScript function called “doLogin()”. This then calls the JavaScript in the HTML above that displays the iframe by doing a POST form submission to YouTube. When that form is submitted, we pass a variable called “next”, this variable is where YouTube will redirect the user. The url that I supplied has a little JavaScript in there that returns the token for the upload back to the parent frame, it’ll be attached soon.
So now we’ve got to the point when the iframe has appeared and the user has entered their log in details. Like I said above, we then take the user to a “next” url, here’s the code for that:
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<script type="text/javascript">
<!--
function getURLVar(name)
{
name = name.replace( /[\[]/, '\\\[' ).replace( /[\]]/ , '\\\]' );
var regexS = '[\\?&]' + name + '=([^&#]*)';
var regex = new RegExp( regexS );
var results = regex.exec( window.location.href );
if ( results == null )
{
return '';
}
else
{
return results[1];
}
}
function getToken()
{
var token = getURLVar( 'token' );
return token;
}
-->
</script>
<title>Auth Response
</title>
</head>
<body>
Please wait...
<script type="text/javascript">
<!--
parent.handleLogin( getToken() );
-->
</script>
</body>
</html>
This is quite a simple script: it simply looks for the “token” variable in the url and then passes that back to the parent.
The parent then closes the iframe and then we’re back to the flash application as the parent fires the “handleLoginComplete()” function passing the token to the flash. The flash application then prepares the meta data view, this is simply where the user adds the details about the video and also selects the category from a “ComboBox()” (that we populated earlier).
Once the user is then happy, they fire the “sendVideoData()” function that prepares an XML-RPC call to YouTube, where we pass the meta data as XML, as well as the auth token and your developer key.
Once that call has been made, we’re then given an upload url and upload token. We take this and populate a form in HTML with the data.
Now the reason we use a HTML form to do the actual uploading is that we need to use the iframe to get the video id back from YouTube of the uploaded video. So, you’ll see that “handleVideoDataResult()” calls a function called “showUpload()”, which shows the browse form and populates the data. Once the user has chosen the file, we then post the form by calling the “doUpload()” from the flash.
If you look carefully in the “showUpload()” function, you’ll see this line:
$('#youtubeUploadForm').attr( 'action', url + '?nexturl=http://localhost:9852/upload-response.html' );
We pass a “nexturl” to the uploader that we then use to get the video id. So much like the “auth-response.html”, the “upload-response.html” gets the “id” var, like so:
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<script type="text/javascript">
<!--
function getURLVar(name)
{
name = name.replace( /[\[]/, '\\\[' ).replace( /[\]]/ , '\\\]' );
var regexS = '[\\?&]' + name + '=([^&#]*)';
var regex = new RegExp( regexS );
var results = regex.exec( window.location.href );
if ( results == null )
{
return '';
}
else
{
return results[1];
}
}
function getVideoId()
{
var token = getURLVar( 'id' );
return token;
}
-->
</script>
<title>Upload Response
</title>
</head>
<body>
Please wait...
<script type="text/javascript">
<!--
parent.handleUpload( getVideoId() );
-->
</script>
</body>
</html>
And again, this is passed back to the parent, and the parent is able to close down the frame and pass it back to the flash!
So there we have it, a quick and dirty run through of how to upload a video to YouTube! The code is available on Github. And if you get the code of Github, it includes a Ruby server for nice debugging.
Here’s a juicy example for you to mull over:
Tell me what you think.