Mobile Me(dia)

Shawn Van Every Shawn.Van.Every@nyu.edu
Spring 2008
H79.2690.1

Week 5 - Transcoding/Thumbnailing

Often when recieving files from users, they aren't in the ideal format to deliver to other mobile devices or present on the web.

Transcoding is the conversion from one format to another and is what we need to do to convert an MPEG-4 into a 3GP or FLV file.

Unfortunately, while their exist great tools for file conversion on the desktop (QuickTime Pro) doing such on the server side (automatically) can be considerably more complex. FFMPEG is generally considered to be the best open source transcoding tool available for use on the command line. Unfortunately, it is difficult to configure and install as well as keep up to date (if you are interested in installing it on your own host, I would be happy to help).

Thankfully a new web service called Mux has done much of the hard work for us.

Transcoding with Mux

First of all, to use Mux you need to register on their site and apply for an ID and a Key

They will email you your ID and Key after you are approved (I talked to them, they should approve anyone from this class).

Following that, we can start programming:
<?PHP
	include "Snoopy.class.php";
	
	$media_url = "http://itp.nyu.edu/~sve204/mobilemedia/php_popper/posts/1202423098_9136_057.3g2";
	
	$mux_api_key = "xxxxx";
	$mux_api_caller_id = "xxxxx";
	
	$callback_page = "http://your.server.com/callbackpage.php";

	$mux_api_key_encoded = base64_encode($mux_api_caller_id . ":" . $mux_api_key);

	$snoopy = new Snoopy;
	
	$api_endpoint = "http://mux.am/api/endpoint/1/basic/";
	$command = "TranscodeMediaUrlRequest";

	$submit_url = $api_endpoint . $command;

	$submit_vars = array();
	$submit_vars['clr'] = $mux_api_key_encoded;
	$submit_vars['url'] = $media_url;
	$submit_vars['cb'] = $callback_page;

	$submit_vars['sot'] = "video/quicktime"; // Output formats
	/*
		video/mp4 MPEG-4
		video/quicktime Quicktime
		video/m4v iPod (M4V)
		video/x-flv Flash Video
		video/x-ms-wmv Windows Media
		video/mpg MPEG-1/2
		video/avi Windows AVI
		video/3gpp 3GP Mobile
		application/ogg OGG Theora
		video/mp4-psp PSP MPEG-4
		
		image/jpeg JPEG Thumbnail
	*/
	
	// These are optional
	//$submit_vars['smt'] = "video/3gpp2"; // Input mime type, not required
	$submit_vars['em'] = "xxx@xxx.com"; // Email address for notification
	$submit_vars['h'] = 240; // output height, default is input height
	$submit_vars['w'] = 320; // output width, default is input width
	
	//$submit_vars['s'] = 0; // starting point in seconds, omit for 0
	//$submit_vars['l'] = 100; // length to transcode, omit for whole thing
	//$submit_vars['br'] = 40; // kilobytes per second
	//$submit_vars['fps'] = 10; // frames per second
	
	if ($snoopy->submit($submit_url,$submit_vars))
	{
		echo "<PRE>".$snoopy->results."</PRE>\n";
		/*
		Should look like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<jobStartedResponse>
    <jobId>0AFD0FCF01183BDD6EA3F94E014741A3</jobId>
</jobStartedResponse>
		*/
		
		// Get the job id:
		$matches = array();
		if (preg_match('/<jobId>(.*)<\/jobId>/',$snoopy->results,$matches) > 0 && sizeof($matches) > 1)
		{
			$mux_job_id = $matches[1];
			
		}
		else
		{
			echo "Sorry, something went wrong with mux";
		}

		// You would use the job id on the callback page to know which job was finished

		
		
	}
	else
	{
		echo "error fetching document: ".$snoopy->error."\n";
	}
?>		
You can run this snippet of code from the command line and see that it makes a request to them for transcoding the file specified. You will need the Snoopy.class.php file in the same directory as this file for it to work correctly.

More Information: Mux Developer Guide
Older Mux Developer Guide (what we are using now)
The above code snippet can be easily integrated into our upload script such as follows.

Integrating into upload script
<?PHP
	echo "<?xml version=\"1.0\"?>\n"; 

	// Turn on error reporting
	ini_set('display_errors', true);
	ini_set('display_startup_errors', true);
	// You could also log them:
	//ini_set('log_errors', true);
	//ini_set('error_log', '/home/netid/php_errors.log');
	error_reporting(E_ALL);

	// Database connect function
	$mySql = null;
	function sqlConnect() {
		# Configuration Variables
		$hostname = "localhost";
		$dbname = "xxxx";
		$username = "xxxx";
		$password = "xxxx";
		
		$mySql = mysql_connect($hostname, $username, $password) or die (mysql_error());
		mysql_select_db($dbname, $mySql) or die(mysql_error());
		
		return $mySql;
	}

?>
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<title>Upload Transcode Test</title>
	</head>
	
	<body>
		<h1>File Upload Transcode Test</h1>
<?

	// Limit what people can upload for security reasons and because we only want video files				
	$allowed_mime_types = array("video/3gpp"=>"3gp", 
								"video/mp4"=>"mp4",
								"video/3gpp2"=>"3g2",
								"video/mpeg"=>"mpg",
								"video/quicktime"=>"mov",
								"video/x-quicktime"=>"mov",
								"video/x-msvideo"=>"avi",
								"audio/vnd.wave"=>"wav"		
								);

	// Make sure form was submitted
	if (isset($_POST['form_submitted']) && $_POST['form_submitted'] == "true")
	{
		// Check the mime type
		$allowed_types = array_keys($allowed_mime_types);
		$allowed = false;
		if (isset($_FILES['bytes']['type']))
		{		
			for ($i = 0; $i < sizeof($allowed_types) && !$allowed; $i++)
			{
				if (strstr($_FILES['bytes']['type'], $allowed_types[$i]))
				{
					$allowed = true;
				}
			}
		
			// If the mime type is good, save it..
			if ($allowed)
			{
				$uploadfilename = time() . "_" . rand(1000,9999) . "_" . basename($_FILES['bytes']['name']);
				// Make sure apache can write to this folder
				$uploaddir = '/xxxx/xxxx/xxxx/xxxx/xxxx/xxxx/';
				$uploadfile = $uploaddir . $uploadfilename;
				$uploadrelativefile = 'http://itp.nyu.edu/xxx/xxx/xxx/xxx/' . $uploadfilename;
		
				if (move_uploaded_file($_FILES['bytes']['tmp_name'], $uploadfile))
				{
					// Make sure the file isn't executable and you can delete it if you need
					chmod($uploadfile, 0666);
					
					// Put it in the database
					/* Database Table:
						mobile_me_messages
						message_id int(11) auto_increment
						attachment varchar(255)
						subject varchar(255)
						body varchar(255)
						from_name varchar(255)
						from_domain varchar(255)
						thumbnail_url varchar(255)
						quicktime_url varchar(255)
						mux_thumbnail_job_id varchar(255)
						mux_transcode_job_id varchar(255)
					*/
					
					$subject = "";
					$message_text = "";
					
					if (isset($_POST['subject']))
					{
						$subject = $_POST['subject'];
					}
					
					if (isset($_POST['message_text']))
					{
						$message_text = $_POST['message_text'];					
					}
					
					// Connect to the database
					$mySql = sqlConnect();
										
					// Insert Data
					$query = "insert into mobile_me_messages (attachment, subject, body, from_name, from_domain) values ('" . $uploadfile . "', '" . $subject . "', '" . $message_text . "', 'mobile web user', 'mobile web user')";
					$result = mysql_query($query, $mySql);
					
					// Get the last insert id so that we can update the record
					$mobile_me_messages_id = mysql_insert_id($mySql);
					
					// Now do the mux transcode request
					include "Snoopy.class.php";
					
					// The media_url is the file that was just uploaded
					$media_url = $uploadrelativefile;
					
					// The mux keys
					$mux_api_key = "xxxxx";
					$mux_api_caller_id = "xxxxx";
					$mux_api_key_encoded = base64_encode($mux_api_caller_id . ":" . $mux_api_key);
	
					// The page that mux calls back when done
					$callback_page = "http://itp.nyu.edu/xxxx/xxxx/xxxx/muxcallback.php";

					// Snoopy is charlie brown's friend								
					$snoopy = new Snoopy;
					
					// What we are hitting with snoopy (mux)
					$api_endpoint = "http://beta2.mux.am/api/endpoint/1/basic/";
					$command = "TranscodeMediaUrlRequest";
				
					$submit_url = $api_endpoint . $command;
				
					// Setup the array of variables to send to mux
					$submit_vars = array();
					$submit_vars['clr'] = $mux_api_key_encoded;
					$submit_vars['url'] = $media_url;
					$submit_vars['cb'] = $callback_page;
				
					// What format do we want back?
					$submit_vars['sot'] = "video/quicktime"; // Output formats
					/*
						video/mp4 MPEG-4
						video/quicktime Quicktime
						video/m4v iPod (M4V)
						video/x-flv Flash Video
						video/x-ms-wmv Windows Media
						video/mpg MPEG-1/2
						video/avi Windows AVI
						video/3gpp 3GP Mobile
						application/ogg OGG Theora
						video/mp4-psp PSP MPEG-4
						
						image/jpeg JPEG Thumbnail
					*/
					
					// Email address for notification
					$submit_vars['em'] = "xxxx@xxxx.com"; 

					$submit_vars['w'] = 320; // output width, default is input width					
					$submit_vars['h'] = 240; // output height, default is input height
					
					//$submit_vars['s'] = 0; // starting point in seconds, omit for 0
					//$submit_vars['l'] = 100; // length to transcode, omit for whole thing
					//$submit_vars['br'] = 40; // kilobytes per second
					//$submit_vars['fps'] = 10; // frames per second
					
					if ($snoopy->submit($submit_url,$submit_vars))
					{
						//echo "<PRE>".$snoopy->results."</PRE>\n";
						/*
						Should look like this:
							<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
							<jobStartedResponse>
								<jobId>0AFD0FCF01183BDD6EA3F94E014741A3</jobId>
							</jobStartedResponse>
						*/
						
						// Get the job id:
						$matches = array();
						if (preg_match('/<jobId>(.*)<\/jobId>/',$snoopy->results,$matches) > 0 && sizeof($matches) > 1)
						{
							$mux_job_id = $matches[1];
							echo "Mux Job ID: " . $mux_job_id;	
						}
						else
						{
							echo "Sorry, something went wrong with mux";
							$mux_job_id = '0';
						}
							
						// Put the job id in the database
						$sql = "update mobile_me_messages set mux_transcode_job_id = '$mux_job_id' where message_id = $mobile_me_messages_id";
						$result = mysql_query($sql, $mySql);
			
					}
					else
					{
						echo "error fetching document: ".$snoopy->error."\n";
					}
					
					
					// Disconnect from the database
					mysql_close($mySql);					
					
					// Tell the user
					echo "<p>Success <br /> <a href=\"" . $uploadrelativefile  . "\">" . $uploadrelativefile . "</a></p>";
				}
				else
				{
					echo "<p>Error on upload...!  Here is some debugging info:</p>";
					var_dump($_FILES);
				}
			}
			else
			{
				echo "<p>Type not allowed...! Here is some debugging info:</p>";
				var_dump($_FILES);
			}
		}
		else
		{
			echo "<p>Strange, file type not sent by browser...!  Here is some debugging info:</p>";
			var_dump($_FILES);
		}
	}
	else
	{
?>
		<form enctype="multipart/form-data" method="post" action="uploadmux.php">
			<p>
				Title: <input type="text" name="subject" /><br />
				Description: <input type="text" name="message_text" /><br />
				<input type="file" name="bytes" />
				<input type="hidden" name="form_submitted" value="true" />
				<br />
				<input type="submit" name="submit" value="submit" />
			</p>
		</form>
<?
	}
?>
	</body>
</html>		
		
Callback page

Submitting the media to Mux for transcoding is half the battle. What we need to do next is get the media back from them when it is done.

For these purposes they have implemented a "callback". Essentially, you specify a URL when you submit your media to them and they call that URL with the URL to the transcoded media when they are done.

In the above example, the purpose of the $callback_url variable is just that, to tell them what page to hit when the trancoding is complete:
$callback_page = "http://itp.nyu.edu/~sve204/mobilemedia/muxtranscoder/muxcallback.php";		
		
Here are the contents of muxcallback.php:
<?PHP

	// Extra Database connect function
	$mySql = null;
	function sqlConnect() {
		# Configuration Variables
		$hostname = "localhost";
		$dbname = "xxxx";
		$username = "xxxx";
		$password = "xxxx";
		
		$mySql = mysql_connect($hostname, $username, $password) or die (mysql_error());
		mysql_select_db($dbname, $mySql) or die(mysql_error());
		
		return $mySql;
	}
	
	// Let's save it to a file so we can see what mux is sending back and get some output for ourselves to debug with
	// We have to look at $HTTP_RAW_POST_DATA to get what mux sends us
	$filename = "/xxx/xxxx/xxxx/xxxx/xxx/xxxx/muxcallback.txt";
	$filehandle = fopen($filename, 'w');	
	$raw_input_data = print_r($HTTP_RAW_POST_DATA,true);
	fwrite($filehandle, $raw_input_data);	
	
	
	/*
	Should look like this when done:
	<transcodeCompletion>
		<url>http://mux.am/files/fooey</url>
		<expires>TODO</expires>
	</transcodeCompletion>		
	*/
	
	// Get the job id:
	$jobid_matches = array();
	if (preg_match('/<jobId>(.*)<\/jobId>/',$HTTP_RAW_POST_DATA,$jobid_matches) > 0 && sizeof($jobid_matches) > 1)
	{
		$mux_transcoded_jobid = $jobid_matches[1];
		fwrite($filehandle, "Job ID Match: " . $mux_transcoded_jobid . "\n");
	
		$url_matches = array();
		if (preg_match('/<url>(.*)<\/url>/',$HTTP_RAW_POST_DATA,$url_matches) > 0 && sizeof($url_matches) > 1)
		{
			$mux_transcoded_url = $url_matches[1];
			fwrite($filehandle, "URL Match: " . $mux_transcoded_url . "\n");
			
			$mux_transcoded_url = str_replace('&','&',$mux_transcoded_url);				
			$mux_url = parse_url($mux_transcoded_url);
				
			// DO DOWNLOAD
			// Setup some variables
			$transcoded_filename = "muxoutput_" . time() . rand(1000,9999) . ".mov"; // Specifying .mov here, not the best thing to do as we don't know the format
			$transcoded_file = "/xxx/xxxx/xxx/xxx/xxx/xxx/" . $transcoded_filename;
			$transcoded_file_relative = "http://itp.nyu.edu/xxx/xxx/xxxx/xxxx/" . $transcoded_filename;

			//$ch = curl_init($mux_url['scheme'] . "://" . $mux_url['host'] . ":" . $mux_url['port'] . "/" . $mux_url['path'] . "?" . urldecode($mux_url['query']));
			$ch = curl_init($mux_transcoded_url);
			$fp = fopen($transcoded_file, "w");
				
			curl_setopt($ch, CURLOPT_FILE, $fp);
			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
			curl_setopt($ch, CURLOPT_HEADER, 0);
			
			curl_exec($ch);

			//echo curl_getinfo($ch,CURLINFO_EFFECTIVE_URL) . "<br />\n";

			curl_close($ch);
			fclose($fp);			
				
			fwrite($filehandle, "File Saved: " . $transcoded_file_relative . "\n");
				
			// UPDATE DATABASE
			$mySql = sqlConnect();
			$sql = "update mobile_me_messages set quicktime_url = '" . $transcoded_file_relative . "' where  mux_transcode_job_id = '" . $mux_transcoded_jobid . "'";
			$result = mysql_query($sql, $mySql);
			
			fwrite($filehandle, "SQL Statement: " . $sql . "\n");
		}
	}
	
	fclose($filehandle);
	
?>
OK		
		
This script, receives the XML POST that is sent by Mux, parses it looking for the "jobId" and then the URL of the media. Once those are found, it downloads the media and updates the database.

You will notice that I added two extra fields into the database to deal with the transcoded file. First is the "mux_transcode_job_id" so that when I submit I can keep track of requests to and from them and the second is the "quicktime_url" which holds the relative filename of the transcoded file after it is downloaded.

To get this script to work you will have to update the following variables in the code with the appropriate locations on your server (don't forget the database connection information as well):

$filename should contain the path to a world writable file that will hold debugging output for this script. Since we are not running this from a web browser, rather it is getting hit by the mux service automatically, this is one way we can see what is happening.
$filename = "/xxx/xxxx/xxxx/xxxx/xxx/xxxx/muxcallback.txt";

$transcoded_file should contain the path to a directory that is world writable where the file that is downloaded from mux will be saved.
$transcoded_file = "/xxx/xxxx/xxx/xxx/xxx/xxx/" . $transcoded_filename;

$transcoded_file_relative should be the corresponding web viewable url to the directory specified in $transcoded_file. In both cases, be sure to leave the ". $transcoded_filename" in place.

$transcoded_file_relative = "http://itp.nyu.edu/xxx/xxx/xxxx/xxxx/" . $transcoded_filename;

You can download a package with all of these files here

There you have it, we have successfully integrated transcoding into our upload form. Of course, this could be integrated into the parseMailScript just as easily.