How to Create a Resumable Video Uploader in Node.js
videos

How to Create a Resumable Video Uploader in Node.js

Tutorial Details
  • Topic: HTML5 and Node.js
  • Difficulty: Intermediate
  • Estimated Completion Time: About an Hour

If you’ve ever uploaded a considerably large video file, then you know this feeling: you’re 90% done, and accidentally refresh the page – having to start all over again.

In this tutorial, I’ll demonstrate how to make a video uploader for your site that can resume an interrupted upload, and generate a thumbnail upon completion.


Intro

To make this uploader resumable, the server needs to keep track of how much a file has already been uploaded, and be able to continue from where it left off. To accomplish this task, we will give full control to the Node.js server to request specific blocks of data, and the HTML form will pickup these requests and send the necessary information to the server.

To handle this communication, we’ll use Socket.io. If you’ve never heard of Socket.io, it is a framework for real-time communication between Node.js and an HTML web page – well dig more into this shortly.

This is the basic concept; we will start with the HTML form.


Step 1: The HTML

I am going to keep the HTML fairly simple; all we need is an input to choose a file, a text box for the name, and a button to begin the upload. Here’s the necessary code:

	<body> 
		<div id="UploadBox">
			<h2>Video Uploader</h2>
			<span id='UploadArea'>
				<label for="FileBox">Choose A File: </label><input type="file" id="FileBox"><br>
				<label for="NameBox">Name: </label><input type="text" id="NameBox"><br>

				<button	type='button' id='UploadButton' class='Button'>Upload</button>
			</span>
		</div>
	</body>

Notice that I have wrapped the contents in a span; we will use this later to update the page’s layout with JavaScript. I’m not going to cover the CSS in this tutorial, but you can download the source code, if you’d like to use mine.


Step 2: Making it Work

HTML5 is still relatively new, and isn’t yet fully supported in all browsers. The first thing we need to do, before moving forward, is ensure that the user’s browser supports the HTML5 File API and FileReader class.

The FileReader class allows us to open and read parts of a file, and pass the data as a Binary string to the server. Here is the JavaScript for the feature detection:

	window.addEventListener("load", Ready); 
	
	function Ready(){ 
		if(window.File && window.FileReader){ //These are the relevant HTML5 objects that we are going to use 
			document.getElementById('UploadButton').addEventListener('click', StartUpload);  
			document.getElementById('FileBox').addEventListener('change', FileChosen);
		}
		else
		{
			document.getElementById('UploadArea').innerHTML = "Your Browser Doesn't Support The File API Please Update Your Browser";
		}
	}

The code above additionally adds event handlers to the button and file input in the form. The FileChosen function simply sets a global variable with the file – so that we can access it later – and fills in the name field, so that the user has a reference point when naming the file. Here is the FileChosen function:

	var SelectedFile;
	function FileChosen(evnt) {
        SelectedFile = evnt.target.files[0];
		document.getElementById('NameBox').value = SelectedFile.name;
    }

Before we write the StartUpload function, we have to setup the Node.js server with socket.io; let’s take care of that now.


Step 3: The Socket.io Server

As I mentioned earlier, I’ll be using Socket.io for communication between the server and the HTML file. To download Socket.io, type npm install socket.io into a Terminal window (assuming that you’ve installed Node.js), once you have navigated to this projects directory. The way socket.io works is: either the server or the client “emits” an event, and then the other side will pickup this event in the form of a function with the option of passing JSON data back and forth. To get started, create an empty JavaScript file, and place the following code within it.

	var app = require('http').createServer(handler)
	  , io = require('socket.io').listen(app)
	  , fs = require('fs')
	  , exec = require('child_process').exec
	  , util = require('util')
	
	app.listen(8080);

	function handler (req, res) {
	  fs.readFile(__dirname + '/index.html',
	  function (err, data) {
	    if (err) {
	      res.writeHead(500);
	      return res.end('Error loading index.html');
	    }
	    res.writeHead(200);
	    res.end(data);
	  });
	}
	
	io.sockets.on('connection', function (socket) {
		//Events will go here
	});

The first five lines include the required libraries, the next line instructs the server to listen on port 8080, and the handler function simply passes the contents of our HTML file to the user, when he accesses the site.

The last two lines are the socket.io handler and will be called when someone connects, via Socket.io.

Now, we can go back to the HTML file and define some socket.io events.


Step 4: Some Socket.io Events

To begin using Socket.io in our page, we first need to link to its JavaScript library. You do this in the same way that you would reference any library: reference it in the head area. Add the following to the page, before your scripts, obviously.

<script src="/socket.io/socket.io.js"></script>

Don’t worry about getting this file, as it is generated at runtime by the Node.js server.

Now, we can write the StartUpload function that we connected to our button:

	var socket = io.connect('http://localhost:8080');
	var FReader;
	var Name;
	function StartUpload(){
		if(document.getElementById('FileBox').value != "")
		{
			FReader = new FileReader();
			Name = document.getElementById('NameBox').value;
			var Content = "<span id='NameArea'>Uploading " + SelectedFile.name + " as " + Name + "</span>";
			Content += '<div id="ProgressContainer"><div id="ProgressBar"></div></div><span id="percent">0%</span>';
			Content += "<span id='Uploaded'> - <span id='MB'>0</span>/" + Math.round(SelectedFile.size / 1048576) + "MB</span>";
			document.getElementById('UploadArea').innerHTML = Content;
			FReader.onload = function(evnt){
				socket.emit('Upload', { 'Name' : Name, Data : evnt.target.result });
			}
			socket.emit('Start', { 'Name' : Name, 'Size' : SelectedFile.size });
		}
		else
		{
			alert("Please Select A File");
		}
	}

The first line connects to the Socket.io server; next, we’ve created two variables for the File Reader and the name of the file, as we are going to need global access to these. Inside the function, we first ensured that the user selected a file, and, if they did, we create the FileReader, and update the DOM with a nice progress bar.

The FileReader’s onload method is called every time it reads some data; all we need to do is emit an Upload event, and send the data to the server. Finally, we emit a Start event, passing in the file’s name and size to the Node.js server.

Now, let’s return to the Node.js file, and implement handlers for these two events.


Step 5: Handling The Events

You have to clear the buffer every so often, or the server will crash, due to memory overload.

The socket.io events go inside the handler that we have on the last line of our Node.js file. The first event that we’ll implement is the Start event, which is triggered when the user clicks the Upload button.

I mentioned earlier that the server should be in control of which data it wants to receive next; this will allow it to continue from a previous upload that was incomplete. It does this by first determining whether there was a file by this name that didn’t finish uploading, and, if so, it will continue from where it left off; otherwise, it will start at the beginning. We’ll pass this data in half-megabyte increments, which comes out to 524288 bytes.

In order to keep track of different uploads happening at the same time, we need to add a variable to store everything. To the top of your file, add var Files = {};' Here’s the code for the Start event:

	socket.on('Start', function (data) { //data contains the variables that we passed through in the html file
			var Name = data['Name'];
			Files[Name] = {  //Create a new Entry in The Files Variable
				FileSize : data['Size'],
				Data	 : "",
				Downloaded : 0
			}
			var Place = 0;
			try{
				var Stat = fs.statSync('Temp/' +  Name);
				if(Stat.isFile())
				{
					Files[Name]['Downloaded'] = Stat.size;
					Place = Stat.size / 524288;
				}
			}
	  		catch(er){} //It's a New File
			fs.open("Temp/" + Name, "a", 0755, function(err, fd){
				if(err)
				{
					console.log(err);
				}
				else
				{
					Files[Name]['Handler'] = fd; //We store the file handler so we can write to it later
					socket.emit('MoreData', { 'Place' : Place, Percent : 0 });
				}
			});
	});

First, we add the new file to the Files array, with the size, data and amount of bytes downloaded so far. The Place variable stores where in the file we are up to – it defaults to 0, which is the beginning. We then check if the file already exists (i.e. it was in the middle and stopped), and update the variables accordingly. Whether it’s a new upload or not, we now open the file for writing to the Temp/ folder, and emit the MoreData event to request the next section of data from the HTML file.

Now, we need to add the Upload event, which, if you remember, is called every time a new block of data is read. Here is the function:

	socket.on('Upload', function (data){
			var Name = data['Name'];
			Files[Name]['Downloaded'] += data['Data'].length;
			Files[Name]['Data'] += data['Data'];
			if(Files[Name]['Downloaded'] == Files[Name]['FileSize']) //If File is Fully Uploaded
			{
				fs.write(Files[Name]['Handler'], Files[Name]['Data'], null, 'Binary', function(err, Writen){
					//Get Thumbnail Here
				});
			}
			else if(Files[Name]['Data'].length > 10485760){ //If the Data Buffer reaches 10MB
				fs.write(Files[Name]['Handler'], Files[Name]['Data'], null, 'Binary', function(err, Writen){
					Files[Name]['Data'] = ""; //Reset The Buffer
					var Place = Files[Name]['Downloaded'] / 524288;
					var Percent = (Files[Name]['Downloaded'] / Files[Name]['FileSize']) * 100;
					socket.emit('MoreData', { 'Place' : Place, 'Percent' :  Percent});
				});
			}
			else
			{
				var Place = Files[Name]['Downloaded'] / 524288;
				var Percent = (Files[Name]['Downloaded'] / Files[Name]['FileSize']) * 100;
				socket.emit('MoreData', { 'Place' : Place, 'Percent' :  Percent});
			}
		});

The first two lines of this code update the buffer with the new data, and update the total bytes downloaded variable. We have to store the data in a buffer and save it out in increments, so that it doesn’t crash the server due to memory overload; every ten megabytes, we will save and clear the buffer.

The first if statement determines if the file is completely uploaded, the second checks if the buffer has reached 10 MB, and, finally, we request MoreData, passing in the percent done and the next block of data to fetch.

Now, we can go back to the HTML file and implement the MoreData event and update the progress.


Step 6: Keeping Track of the Progress

I created a function to update the progress bar and the amount of MB uploaded on the page. In addition to that, the More Data event reads the block of data that the server requested, and passes it on to the server.

To split the file into blocks, we use the File API’s Slice command. Since the File API is still in development, we need to use webkitSlice and mozSlice for Webkit and Mozilla browsers, respectively.

	socket.on('MoreData', function (data){
		UpdateBar(data['Percent']);
		var Place = data['Place'] * 524288; //The Next Blocks Starting Position
		var NewFile; //The Variable that will hold the new Block of Data
		if(SelectedFile.webkitSlice) 
			NewFile = SelectedFile.webkitSlice(Place, Place + Math.min(524288, (SelectedFile.size-Place)));
		else
			NewFile = SelectedFile.mozSlice(Place, Place + Math.min(524288, (SelectedFile.size-Place)));
		FReader.readAsBinaryString(NewFile);
	});
	
	function UpdateBar(percent){
		document.getElementById('ProgressBar').style.width = percent + '%';
		document.getElementById('percent').innerHTML = (Math.round(percent*100)/100) + '%';
		var MBDone = Math.round(((percent/100.0) * SelectedFile.size) / 1048576);
		document.getElementById('MB').innerHTML = MBDone;
	}

With this final function, the uploader is completed! All we have left to do is move the completed file out of the Temp/ folder and generate the thumbnail.


Step 7: The Thumbnail

Before we generate the thumbnail, we need to move the file out of the temporary folder. We can do this by using file streams and the pump method. The pump method takes in a read and write stream, and buffers the data across. You should add this code where I wrote ‘Generate Thumbnail here’ in the Upload event:

	var inp = fs.createReadStream("Temp/" + Name);
	var out = fs.createWriteStream("Video/" + Name);
	util.pump(inp, out, function(){
		fs.unlink("Temp/" + Name, function () { //This Deletes The Temporary File
			//Moving File Completed
		});
	});

We’ve added the unlink command; this will delete the temporary file, after we finish copying it. Now onto the thumbnail: we’ll use ffmpeg to generate the thumbnails, because it can handle multiple formats, and is a cinch to install. At the time of this writing, there aren’t any good ffmpeg modules, so we’ll use the exec command, which allows us to execute Terminal commands from within Node.js.

	exec("ffmpeg -i Video/" + Name  + " -ss 01:30 -r 1 -an -vframes 1 -f mjpeg Video/" + Name  + ".jpg", function(err){
		socket.emit('Done', {'Image' : 'Video/' + Name + '.jpg'});
	});

This ffmpeg command will generate one thumbnail at the 1:30 mark, and save it to the Video/ folder with a .jpg file type. You can edit the time of the thumbnail by changing the -ss parameter. Once the thumbnail has been generated, we emit the Done event. Now, let’s go back to the HTML page and implement it.


Step 8: Finishing Up

The Done event will remove the progress bar and replace it with the thumbnail image. Because Node.js is not setup as a web server, you have to place the location of your server (e.g. Apache) in the Path variable, in order to load the image.

	var Path = "http://localhost/";
	
	socket.on('Done', function (data){
		var Content = "Video Successfully Uploaded !!"
		Content += "<img id='Thumb' src='" + Path + data['Image'] + "' alt='" + Name + "'><br>";
		Content += "<button	type='button' name='Upload' value='' id='Restart' class='Button'>Upload Another</button>";
		document.getElementById('UploadArea').innerHTML = Content;
		document.getElementById('Restart').addEventListener('click', Refresh);
	});
	function Refresh(){
		location.reload(true);
	}

Above, we’ve added a button to begin uploading another file; all this does is refresh the page.


Conclusion

That’s all there is to it, but, surely, you can imagine the possibilities when you pair this up with a database and an HTML5 player!

I hope you’ve enjoyed this tutorial! Let me know what you think in the comment section below.

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Mario

    Very, very cool . First ever first comment on Net Tuts lol.

  • Adri

    Thank you. Really interesting. I love you guys when you post nodejs things!

  • http://www.mitrastudios.com mohanraaj

    Nice tut… i really found hard on working with node.js

    Thank you Gabriel…

  • PNTR

    Thanks! Node.js seems interesting but havn’t had time to look in to it yet that much. So this is a nice article. Hoping for more.

  • Tony

    This looks awesome! Node rocks \m/ – \m/   !

  • http://raxezdev.com Jesper

    Very nice! Bring us more Node.js content, I love it!

  • http://www.udgwebdev.com Caio Ribeiro Pereira

    Nice post man!! This will be very useful for my future projects!

  • thecodingdude

    YAY! More Node please! :)

  • http://dalehurley.com Dale

    Freaky – I was just thinking about this problem this morning. I was going to try to build it in CodeIgniter but NODE.JS makes so much more sense.

  • http://www.fleet-management-solutions.net Callie Walls

    Its affordale and realiable

    Thanks Sir

  • http://www.metacrash.com.au Marc Loney

    This is a great introductory tutorial showing the power of node.js streams when constructing realtime web applications. Excellent work!

    I also really appreciate how you decided to use raw JS on the client-side rather than abstract working with the File API to a jQuery plug-in. There seems to be this growing trend for developers to reach for a hammer every. single. time. they need to manipulate the DOM, when it’s clearly not necessary.

    For those of you looking to implement this in production, be aware that you will need to offer a fallback for browsers without File API support. This tutorial doesn’t go into that but it’s fairly trivial to implement it when you perform feature detection.

  • andrei

    I liked the article but I cannot refrain myself from commenting on your bad coding style (just my opinion) variables shouldn’t start with uppercase, camelCase should always start with small letter, objects should be accessed using the dot notation not like arrays, if/else declarations should be inside {} even if it’s just one declaration inside. I simply gave up on reading your code I’m doing my own version and reading along if i get stuck. If your using sublime text i recommend using JSHint and JSLint, it highlights sections of code with problems and helps you avoid errors.

    • bentgunnar

      +1 the code is solid. Obviously the head was in just the right place when this little recipe got baked, but the meaningless spacing and code arrangement is like what the hell happened. Like the if-statement layout and plenty else is just awful bro. get your shit together, no hate, just love and respect after you dropped some of the best socket.io knowledge on us all, but seriously https://github.com/fuckit/js-standards

  • http://onetutorials.blogspot.com anu

    the great tutorial for beginner

  • Adrian Lee

    This is awesome

  • sam

    Hi this is great.

    Would it be possible to get this to upload directly to amazon s3 or will this not work?

    Thanks

    • http://gabrielmanricks.com Gabriel
      Author

      Ya it sounds possible theoretically, I haven’t really tried it but I have found a nodejs module called knox made by learnboost on github. It seems like knox allows you to upload streams of data to amazon s3, so instead of saving the file to a server you would pass it to the knox s3 stream. Hope this helps

  • Matthew

    This is awesome! Does anyone know how to actually get it working? I’m a beginner and uploaded this to my site’s server, expecting it to work just like that. Is there a piece of the puzzle I’m missing?

    • Gabriel
      Author

      Do you have nodejs installed on your server? If so you have to install socket.io the instructions are in the tutorial and then just run the node file. Let me know if this helps.

  • payne

    hi

    the tut is great but when i upload a video when it reaches the 99.99% it keeps go the MoreData,any idea why?

  • daslicht

    Hello,
    thank you very much for this tutorial.
    Anything works fine.

    util.pump() however is depreciated.

    Here is my approch to fix it :
    http://jsfiddle.net/7FgHh/

    The only think which do not work is the unlink, the file only gets deleted if I restart the node server..

    Another approch is not using a pipe and just a fs.rename e.g:

    fs.rename(“Temp/” + Name, “Video/” + Name, function(){
    fs.unlink(“Temp/” + Name, function ()
    {
    console.log(“unlink this file:”,Name );
    socket.emit(‘Done’, {‘Image’ : ‘Video/’ + Name + ‘.jpg’});
    });
    });

    I would love to know how to make the pipe working :)

  • daslicht

    Is it normal that this kind of upoad is so much slower than just posting data ?

  • daslicht

    Is it normal that this kind of upload is so much slower than just posting data ?

  • Jesper

    Great stuff, works very nicely. one thing to note, “webkitSlice” is deprecated and is now just “slice”. And for ffmpeg stuff I quite like fluent-ffmpeg, I’m new to ffmpeg but pretty its straight forward to make thumbnails and convert video to web friendly format.

    • http://gabrielmanricks.com/ Gabriel
      Author

      Thanks. I thought about using fluent-ffmpeg in the article but I think I ultimately decided against it because I wanted to keep the minimum amount of dependencies and I wasn’t really using any of the advance features, just the “on complete” function. Anyways If you want a good cheat-sheet for ffmpeg in general I sometimes use http://rodrigopolo.com/ffmpeg/cheats.html, it helps with the different sizes and aspect ratios.

      • Jesper

        Hmm, seems the uploaded files get corrupted, almost always 9bytes too long:

        14,610,782 ends up as = 14,610,791 and thus makes the file useless.

        Any ideas on this?

        Cheers

      • http://gabrielmanricks.com/ Gabriel
        Author

        Can you give me a little more info (i.e. is this always or only on resume / filesize / filetype). I will try and look over it on the weekend and try and figure out the problem.

        My initial thoughts are that it probably has something to do with a miscalculated index/size (indexes start at 0 while sizes do not) or maybe something to do with an http header, but I will have to look into these things.

      • Jesper

        It seems like it always happens, resume / no-resume and filetype dosen’t matter either. I’ll redo everything from scratch later, maybe I’ll find it.

      • Jesper

        Your code is fine, I messed up somewhere I guess :)

      • http://gabrielmanricks.com/ Gabriel
        Author

        Thanks.

  • telnasser

    I tried the code, but I keep getting this response in my browser:

    50% – 0/0MB

    Am I missing anything?

  • http://www.facebook.com/patrick.welborn Patrick Welborn

    Is it just me, or is RequireJS a dependency here but not mentioned at all as part of the tutorial?

  • Alex

    Your code do not run in IE9 and safari.

  • Fuzz

    As I understand this tut the Script wont resumen the upload automatically. The user has to click the upload-Button again. Is that right?

  • vote539

    Thanks for the tutorial. I have a question. According to the W3C spec, as well as the Mozilla docs, the “load” event on a FileReader is supposed to fire only once per file. There is another event, “progress”, that fires more frequently: http://www.w3.org/TR/FileAPI/#dfn-load-event

    By what I can tell from your code, then, the “MoreData” message is sent from the server to the client only twice: once when the upload is initialized (at the “Start” event) and again at the “Upload” event. What part of your application is causing the event to be sent more frequently?

    • http://gabrielmanricks.com/ Gabriel
      Author

      I think you are tight about the load event, but the progress events are from Node.js’s end Even if it only sends the file in one shot, it is split up in chunks automatically, and every time it gets some it will post back a progress event.

      So instead of it being a progress bar of ‘how much was sent’ it is a progress bar of ‘how much Node.js has received ‘ Hope this helps