api.video

Features

Tutorials

Private Video Upload with a public token

December 17, 2020 - Doug

Where I live in the UK, there are miles and miles of footpaths - publically accessible trails that cross private property. There's an expectation that those that use the public footpaths will respect the land/plants/crops of the private owners, in exchange for access to the beautiful countryside.

Public access to api.video

api.video has a concept of delegated tokens which works like a public key - granting anyone with the key access to upload videos into your account. Now, much like the public footpaths, the usage of the public key is expected to be reasonable, and not overused.

But...it is the internet. While most of the users will behave and use the delegated upload appropriately, there might be a bad egg or two that cause issues. To help mitigate public token upload abuse, we have added revocation of delegated tokens. We also have added a 'time to live' where you can provide an expiration time (in seconds) to your token.

So we've wrapped some security around the major vector to delegated token abuse. Even with this change, we have not alleviated all the questions from our customers who are grappling with ideas of privacy with a public token. In this post, we'll show how to address the most common concerns we've heard from our users.

Public, but Private

"Can I have a 'one use' delegated token?

This is a fabulous idea - it completely removes the issue of delegated token abuse - the token "self destructs" upon video upload.

"Can I make a delegated upload private by default?"

A private video has a unique token in the url that allows one view of the video. Each time the video is served, a new URL must be requested from api.video. This prevents the general public from seeing the video, and allows you to decide on the access. By default, all videos are public.

Sometimes, as we walk the footpaths, we come across a path that is marked as private - the landowner does not want the public in that area.

Setting a public, delegated token to private videos is like the "No Trespassing sign" on the footpath.

"Can I disable mp4 download link by default?"

By default, the mp4Support parameter is set to true, allowing the download of a private video. By setting this to false, you can prevent the download from occurring. Another "keep out" sign on the public path, if you will.

Private access, but with the public token??

Can we make the video private and disable the mp4 while using the public delegated token? You can!

Can we create a single-use delegated token - one per video upload? Yes, we can (to a degree). But I think this approach is close enought to a unique upload token to be secure and protect your account from bad actors on the internet.

Demo Time

We've created a demo showing a public/private upload. You can try it out yourself at privatelyupload.a.video.

When you visit this page, you see a form for video upload. I am writing this post in mid-December, so will use a trailer from a classic Christmas movie in my upload:

When I begin the upload (by selecting the video), I create an XMLHTTPRequest to the server with the video name, description, and if the video should be public/private, support/not support mp4:

function getDelegatedToken(title, description, mp4, public){
            videoId="";
			console.log(title, description, mp4, public);
			var qReq = new XMLHttpRequest();
			
			var jsonUpload = { "title": title,
								"description": description,
								"mp4Support": mp4,
								"public": public
								};

			console.log("json", jsonUpload);
			qReq.open("POST", "/createVideo");
			qReq.setRequestHeader("Content-type", "application/json");
			qReq.onload = function (oEvent) {
				

                                   // omitted for clarity -we will cover this in a bit


			}
			qReq.send(JSON.stringify(jsonUpload));

Set video privacy

This request is sent to the "createVideo" endpoint on my server. The Node server extracts the JSON from the body, and creates a video container at api.video. When we create the container, we can set the privacy settings for the video to be uploaded - namely if it should support mp4 download, and if the video should be public or private.

This solves 2 of the questions asked above - 'can a delegated upload be set to private?' and 'Can I disable mp4 support?'.

	if(req.body.public === "false"){
		public=false;
	}
	var mp4  = true;
	if(req.body.mp4Support === "false"){
		mp4=false;
	}
	var title = req.body.title;
	var descr = req.body.description;

	client = new apiVideo.Client({ apiKey: apiVideoKey});	
	
	
	let result = client.videos.create(title, {	"title": title, "mp4Support": mp4,
		"public": public, 
		"description": descr,					
	});

The response from the create video has a videoId that we will use as the container for the video we are about to upload. This container is already built with the privacy settings requested on the webpage!

One use upload token

Next, we'll create a delegated token with a TTL of 90 seconds. Note - the TTL support is not yet in the Node SDK (it launched last week!), so we make HTTP requests to the API endpoint.

  1. Authenticate our apiKey. This will return an authentication key in a second request for the token. 2. Create a delegated token with the desired TTL (set to 90s in the code). The response will give us the delegated token, and when it expires ("expiresAt").
var authOptions = {
			method: 'POST',
			url: 'https://ws.api.video/auth/api-key',
			headers: {
				accept: 'application/json'
				
			},
			json: {"apiKey":apiVideoKey}

		}
		console.log(authOptions);	
		request(authOptions, function (error, response, body) {
			if (error) throw new Error(error);
			//this will give me the api key
			
			var authToken = body.access_token;
			console.log(authToken);
			//now use this to generate a delegated toke with a ttl of 90s
			var tokenTTL = 90;
			var tokenOptions = {
				method: 'POST',
				url: 'https://ws.api.video/upload-tokens',
				headers: {
					accept: 'application/json',
					authorization: 'Bearer ' +authToken
				},
				json: {"ttl":tokenTTL}
	
			}
			request(tokenOptions, function (error, response, body) {
				if (error) throw new Error(error);
				var delegatedToken = body.token;
				var tokenExpiry = body.expiresAt;
				console.log("new token", delegatedToken);
				console.log("new token expires", tokenExpiry);
				var tokenVideoIdJson = {"token": delegatedToken,
										"expires":tokenExpiry,
										"videoId": videoId};
				res.setHeader('Content-Type', 'application/json');
				res.end(JSON.stringify(tokenVideoIdJson));

			});

Once these two requests have been made, we then send back a JSON response to the browser with the videoId, and the delegated token information.

Token Expiration

The token expires in 90 seconds - how can we ensure that the video will upload completely in 90s? The answer is that we cannot. However, we'll use a trick. If we break the video to be uploaded into small segments, we just need to ensure that the first segment of the video is uploaded in 90s. The chunkSize is set to 1 MB - so the first 1 MB must be uploaded in 90s.

We're balancing your customers' upload speed with the expiry of the upload token. If 1 MB in 90s is causing failures - you can lengthen the token expiry, or upload smaller chunks of video.
If you think this is too generous - lower the tokenTTL.

Now we can come back to the JavaScript on the page, and the qReq.onload that I omitted above:

qReq.onload = function (oEvent) {
				//once we create the video id do stuff
				var tokenVideoId = JSON.parse(qReq.response);
				
				console.log(tokenVideoId);
				videoId = tokenVideoId.videoId;
				var delegatedToken = tokenVideoId.token;
				var tokenExpires = tokenVideoId.expires;
				document.getElementById("token-information").innerHTML = "New token "+ delegatedToken + " created. <br/>Expires: " +tokenExpires;
				url = "https://sandbox.api.video/upload?token=" + delegatedToken;
				console.log("new url", url);

				file = input.files[0];
				filename = input.files[0].name;
				numberofChunks = Math.ceil(file.size/chunkSize);
				document.getElementById("video-information").innerHTML = "There will be " + numberofChunks + " chunks uploaded."
				var start =0; 
				chunkCounter=0;
				document.getElementById("file-update").innerHTML ="";
				var chunkEnd = start + chunkSize;
				//upload the first chunk to get the videoId
				createChunk(videoId, start);
			}

First we grab the parameters sent from the Node server. For the purposes of the demo, we display the delegated tokenId and the time the token expires (in GMT) on the page.

We then create the delegated video upload url by appending the token to the base url. We determine the number of chunks to upload, and the initial byterange (0-chunkSize) and send the videoId and the videoFile to the createChunk function.

The createChunk function slices up the video from 0 to chunkEnd-1 (the file Slice API lists the first byte IN the slice, and the first byte NOT in the slice). It's easy to get an off by one error here!

			function createChunk(videoId, start, end){
				chunkCounter++;
				console.log("created chunk: ", chunkCounter);
				chunkEnd = Math.min(start + chunkSize , file.size );
				const chunk = file.slice(start, chunkEnd);
				console.log("i created a chunk of video" + start + "-" + chunkEnd + "minus 1	");
				const chunkForm = new FormData();
				console.log("createChunk videoId",videoId +start+end);
				if(videoId.length >0){
					//we have a videoId
					chunkForm.append('videoId', videoId);
					console.log("added videoId");	
					
				}
				chunkForm.append('file', chunk, filename);
				console.log("added file");

				
				//created the chunk, now upload iit
				uploadChunk(chunkForm, start, chunkEnd);
			}

Then the api calls the uploadChunk function that sets the byterange header (off by one warning!) and uploads the video. It also updates the upload progress. When the segment is completed, the counter is incremented, and calls the createChunk function again:

function uploadChunk(chunkForm, start, chunkEnd){
				var oReq = new XMLHttpRequest();
				oReq.upload.addEventListener("progress", updateProgress);	
				oReq.open("POST", url, true);
				var blobEnd = chunkEnd-1;
				var contentRange = "bytes "+ start+"-"+ blobEnd+"/"+file.size;
				oReq.setRequestHeader("Content-Range",contentRange);
				console.log("Content-Range", contentRange);
				function updateProgress (oEvent) {
					if (oEvent.lengthComputable) {  
					var percentComplete = Math.round(oEvent.loaded / oEvent.total * 100);
					
					var totalPercentComplete = Math.round((chunkCounter -1)/numberofChunks*100 +percentComplete/numberofChunks);
					document.getElementById("chunk-information").innerHTML = "Chunk # " + chunkCounter + " is " + percentComplete + "% uploaded. Total uploaded: " + totalPercentComplete +"%";
				//	console.log (percentComplete);
					// ...
				} else {
					console.log ("not computable");
					// Unable to compute progress information since the total size is unknown
				}
				}
				oReq.onload = function (oEvent) {
							// Uploaded.
								console.log("uploaded chunk" );
								console.log("oReq.response", oReq.response);
								var resp = JSON.parse(oReq.response)
								videoId = resp.videoId;
								//playerUrl = resp.assets.player;
								console.log("videoId",videoId);
								
								//now we have the video ID - loop through and add the remaining chunks
								//we start one chunk in, as we have uploaded the first one.
								//next chunk starts at + chunkSize from start
								start += chunkSize;	
								//if start is smaller than file size - we have more to still upload
								if(start<file.size){
									//create the new chunk
									createChunk(videoId, start);
								}
								else{
									//the video is fully uploaded. there will now be a url in the response
									playerUrl = resp.assets.player;
									console.log("all uploaded! Watch here: ",playerUrl ) ;
									document.getElementById("video-information").innerHTML = "all uploaded! Watch the video <a href=\'" + playerUrl +"\' target=\'_blank\'>here</a>" ;

								}
								
				};
				oReq.send(chunkForm);
		
				
				
			}

These two functions dance back and forth - creating slices and uploading them - until the entire video is uploaded. When the video is fully uploaded, api.video begins assembling the video, and encoding the file. The response conatains the player URL for the video - which we put on the page for the viewer to see. If the video is set to private - it will be a private URL with a token - only good for one view.

Private and secure video upload

I think we've done it! A single use public token for video upload, and the ability to protect the video before it is uploaded.

Now you can be sure that your users will properly "stay on the path" with their video uploads.

The code is availabe on Github if you'd like to take a deeper dive, or create your own version. And don't forget to try the demo privatelyupload.a.video.

Was this tutorial/demo app useful to you? Do you have other examples of how to use api.video that you'd live to see? Let us know in our community forum.

Doug

Developer Evangelist

Get started now

Connect your users with videos