background image

Content Delivery for Mobile Devices

Section Feature Image
Posted by juanin - 07 May 2008
Twitter share icon Facebook share icon Google Plus share icon

In the past, delivering content to mobile devices has been a very tricky subject. Developers who came into the mobile world were usually confronted with a new and unknown paradigm, where very little information could be found on how to determine devices' capabilities and to deliver content to them. It was something completely new, and it looked like this information was kept secretly as a precious treasure by those few who had been able to learn it. The aim of this article is to introduce practical examples on how to deliver content to Mobile Devices, making use of sample source code where possible.

In the past, developers had to rely on the very little information that was available on the Internet, and on their own tests via trial and error. Furthermore, the lack of standarisation among phone manufacturers made things even worse, since each of them supported different file formats, and different ways of downloading content, and so on...

As time went by, information slowly started becoming available, communities of Mobile Developers began to get together on mailing-lists, and great utilities like WURFL and others came to save us.

Fortunately, each day more resources for Mobile Developers continue to appear. Great examples of this are mobiForge and DeviceAtlas.

But despite this, we still often lack clear examples on how to achieve certain tasks, or solve particular problems.
So, in this article we present some practical examples on how to deliver content to Mobile Devices, and make use of sample source code where possible.

Use of DeviceAtlas and others goes beyond the scope of this article, but it is recommended to check out the DeviceAtlas tutorial.
Code examples are in PHP, but should easily be ported to other programming languages.

Serving content

Over the years different mobile devices have supported different ways of downloading content, some of them which are today almost deprecated; examples of these include Openwave's Download Fun or Nokia's COD.

Nowadays, practically all modern mobile devices support what we call "direct download", so this is the method I am going to describe in this article, plus some variations which make use of download descriptor files.
There are two main ways of serving files for content delivery:

  • Serving real files on the server's filesystem
  • Using a script or servlet which streams the content

Serving real files on the server's filesystem

Serving real files is probably the most "safe" way of delivering content. It saves the developer from having to mess with Mobile Browsers' different implementations (which may well behave in different ways), by leaving that burden to the Webserver (Apache, IIS, or other). Webservers already know how to do this job, taking care of handling the different types of HTTP requests, and giving back the correct responses on each case.

The "drawback" with this method is that most of the time we will not want to have the content accessible to the whole world. In most cases we will be billing the customer for the content he's downloading, so we don't want other people to be able to freely download it. The idea then, is to store the content outside of the Web server's document root, and when requested, copy it to some temporary location for download. Some time after the customer has downloaded the content, we erase it from its temporary location.

No matter what way the customer buys the content, we will usually end up sending him a WAP Push or a text message with a URL from where the content may be downloaded (MMS may be another option, but it's also out of the scope of this article). A recommended way of doing this, is by using URLs like:


On this example URL, 123456abcdef is a unique ID, which corresponds to our customer's specific request. When the customer requests some content (by sending a text message to some short code, or whatever other method) we insert a record in our database with a unique ID which identifies that request, and indicates what content should be delivered. Then we send him the URL with this unique ID, so when he connects to it, we look for the record which is associated with this ID and can determine what content to serve.

Below is some sample code that could be used for the get.php script (the script to match the URL with the content):

$request_array = explode('/', $_SERVER['REQUEST_URI']);
$unique_id     = $request_array[2];
$dbc = mysql_connect('myHost', 'myUsername', 'myPasswd') or die('Error connecting to MySQL server');
mysql_select_db('myDB', $dbc); 
$sql = "SELECT contentId
		FROM downloadRequests   
		WHERE uniqueID = '$unique_id'";
$result = mysql_query($sql, $dbc);
if(mysql_num_rows($result) > 0) {
	list($content_id) = mysql_fetch_row($result); 
	$filename     = writeTmpContent($content_id);
	$download_url = 'http://'.$_SERVER['SERVER_NAME'].'/tmp/'.$unique_id.'/'.$filename;
else {
	// display some error page

In this sample code we are first obtaining the ID of the content the customer has requested, by using the unique ID from the URL. Then we call the writeTmpContent() function, passing to it the content ID as a parameter. This function should check what file formats and capabilities the device supports (e.g. by using DeviceAtlas, WURFL, or other), and write the correct file to a temporary location under the Web server's document root. On our example we create a directory named like the URL's unique ID under a "tmp" directory. The function returns the filename, which would be something like "image.jpg" or "ringtone.mp3". We then call the displayDownloadPage() function which will display a page to the customer saying something like "Click here to download your content". This text would contain a link to $download_url. Of course, this is just an example, and it assumes a mysql database with a particular schema. You should adapt the code to your own DB schema and needs.

It is highly recommended to use WALL or some other content adaptation engine to display the download page, in order to be able to render the most appropriate markup language for each device, without having to manually implement it.

You may be tempted to use an HTTP redirect to the content file, instead of using this intermediate download page, but it is not recommended to do so, since not all devices support HTTP redirects. Also this download page can give a nice user experience, by placing your company's logo on it, or you could also use it for supplying some supplementatry information or advertisement.

After the user has downloaded the content, we must erase this temporary file, and deactivate the unique ID so that it is not used again.
There may be several approaches to this:

  • One approach is to mark the file on our database as having been downloaded as soon as the customer accesses the get.php script. Then have a cronjob that runs every X minutes, which searches our database for newly downloaded files, and erases them. There is a drawback with this, which is that the user may have not been able to download the content after accessing the get.php script, because of connection problems or other, and our cronjob will erase the temporary file, despite of not having been really downloaded. It is recommended then, to not erase these files before some reasonable time has passed.
  • Other approaches can be to check if the Web server has sent all the file's data to the client (left as an exercise to the reader) or use OMA Download, which will be described later on this article.
  • Some carriers sometimes require that a file must be able to be downloaded x times by the customer, so that the user can retry the download several times in case it does not download completely on some attempt.
    It may be a good idea to register the User-Agent of the device which has downloaded the content, so on further attempts you can check if it's the same User-Agent. If it's not the same User-Agent, it probably means that someone else is trying to download the content, and you probably won't deliver the content in this case (and display some error message to the user).

Note that the appropriate MIME-Types for each file type to be served must be configured on the Webserver.

Some file types have different variations of MIME-Types that can be used.
From some of the most common file types, these are the MIME-Types I have found to be the most compatible with mobile devices:

File extension MIME-Type
jpg image/jpeg
gif image/gif
png image/png
mid audio/midi
amr audio/amr
mmf application/vnd.smaf
mp3 audio/mpeg
qcp audio/vnd.qcelp
jad text/
jar application/java-archive
3gp video/3gpp
3g2 video/3gpp2

Using a script which streams the content

This approach is easier from the point of view that you will be streaming the content to the device, so you don't need to create temporary files, nor run some cronjob which later erases them.
The problem with this approach is that you have to program all the HTTP headers manually; mainly the ones from your response, but in some cases it may be also necessary to handle the client's request headers also.

By testing different combinations of various recommendations to take into account when streaming content, (posted years ago on the wmlprogramming mailing-list) I have found the best way of doing it by:

  • Sending only the Content-Type and File-Length HTTP headers
  • Including the filename on the end of the URL (like "image.jpg" or "ringtone.mp3")

It is important not to use the "Content-Disposition" HTTP header, since some phones refuse to accept content when using it.
By including the filename on the URL, you will trick the phone to think it's a real file and to accept it.

Now, when you send the download URL to the customer, you don't normally know yet what device the customer has, so you don't know what file formats the device will support. Therefore, you can't include the filename on that URL, and once again, you will need an intermediate download page. Once more, we will use a URL like:


This time, when the customer connects to download the content, the get.php script will not create a temporary file, but point to another script which streams the file contents.
Supposing the resultant content to download will be "image.jpg", the intermediate download page could point the customer to a URL like:


The relevant part of code on the download.php script could be something like the following:

// first get the path to the content and it's MIME-Type, based on the URL's unique ID
if (is_file($path_to_content)) {
	header("Content-Type: ".$mime_type);
	header("Content-Length: ".filesize($path_to_content));
else {
	// some error...

Note that this approach doesn't free us from the fact that the user may have connectivity problems (or other) while downloading the content.

This method has been found to work really well, and no device has yet been detected for which it doesn't work, except for Apple's iPhone (when streaming audio and video).
SeeAppendix A for sample code on how to make it work on iPhone.

OMA Download

As we have previously mentioned, we normally don't have much way of knowing if a customer effectively completed to download his content, or if the content was successfully installed/rendered on his device. This is where OMA Download comes into play.

OMA Download, many times referred as OMA DD, is an open standard and application-level protocol that enables reliable content downloads.
It provides the ability for devices to check if the content will be able to be downloadable and usable on the device, and the ability for providers to receive a confirmation of the download status.

It's supported by most modern devices, but you should check on each download if the device supports it, in order to use it or not before using this approach (determine using DeviceAtlas, WURFL, header checking or other).

How it works

OMA Download makes use of a Download Descriptor (that's where the "DD" in OMA DD comes from), which is a simple XML file containing some specific elements. Instead of directly downloading the content, the customer will be pointed to download this descriptor file, which will contain all the relevant information for the device to be able to download the content.

The following list describes the possible elements to be included on the descriptor:

Name Definition
type The MIME media type of the media object
size The number of bytes to be downloaded from the URI
objectURI The URI (usually URL) from which the media object can be loaded
installNotifyURI The URI (or usually URL) to which a installation status report is to be sent, either in case of a successful completion of the download, or in case of a failure
nextURL The URL to which the client should navigate in case the end user selects to invoke a browsing action after the download transaction has completed with either a success or a failure
DDVersion The version of the Download Descriptor technology
name A user readable name of the Media Object that identifies the object to the user
description A short textual description of the media object
vendor The organisation that provides the media object
infoURL A URL for further describing the media object
iconURI The URI of an icon
installParam An installation parameter associated with the downloaded media object

Actually, only the first 3 elements (type, size and objectURI) are mandatory on the Download Descriptor.

The installNotifyURI element is not mandatory on the descriptor definition, but it probably does not make much sense on using OMA Download if you're not going to define this element. This is the URI (usually URL) where the device will post the download status to, and it's the main reason of the creation of this standard.

name and description are very important and useful parameters. When a device downloads the Download Descriptor, it's mobile browser will display some of the descriptor's data, like the file size, and in most cases the name and description. Despite the fact that these two elements are not mandatory on the browser implementation, they are usually implemented since they help the user understand what he's downloading.

nextURL can be very useful too (but is also optional for the browser implementation). Once the download is completed, the browser will be automatically redirected to the specified URL. Some content providers might use it to up-sell more content, or you might want to redirect to instructions on how to use the downloaded content or to some other resource.

This is the content of an example Download Descriptor (taken from the OMA Download specification):

<media xmlns="">

The file extension of the Download Descriptor should be set to .dd, and the MIME-Type must be set to application/vnd.oma.dd+xml.

Once a device downloads the descriptor, it will make some checks to see if the media object can be downloaded and installed/rendered, such as checking if the requierd free space exists and if it supports the specified file type, and then display the relevant information from the descriptor on the device's browser (name, description, size, etc.). The user can then select to download the media object or cancel the download. Depending on what happens with the download, the installNotifyURI will be invoked, and the device will post to it the correspondant Status Code.

This is the list of possible Status Codes:

Status Code Status Message Informative description of Status Code usage
900 Success Indicates to the service that the media object was downloaded and installed successfully.
901 Insufficient memory Indicates to the service that the device could not accept the media object as it did not have enough storage space on the device. This event may occur before or after the retrieval of the media object.
902 User Cancelled Indicates that the user does not want to go through with the download operation. The event may occur after the analysis of the Download Descriptor, or instead of the sending of the Installation Notification (i.e. the user cancelled the download while the retrieval/installation of the media object was in process).
903 Loss of Service Indicates that the client device lost network service while retrieving the Media Object.
905 Attribute mismatch Indicates that the media object does not match the attributes defined in the Download Descriptor, and that the device therefore rejects the media object.
906 Invalid descriptor Indicates that the device could not interpret the Download Descriptor. This typically means a syntactic error.
951 Invalid DDVersion Indicates that the device was not compatible with the "major" version of the Download Descriptor, as indicated in the attribute Version (that is a parameter to the attribute Media).
952 Device Aborted Indicates that the device interrupted, or cancelled, the retrieval of the media object despite the fact that the content should have been executable on the device. This is thus a different case from “Insufficient Memory” and “Non-Acceptable content).
953 Non-Acceptable Content Indicates that after the retrieval of the media object, but before sending the installation notification, the Download Agent concluded that the device cannot use the media object.
954 Loader Error Indicates that the URL that was to be used for the retrieval of the Media Object did not provide access to the Media Object. This may represent for example errors of type server down, incorrect URL and service errors.

So, besides providing the Download Descriptor for the content download (which you will probably generate on the fly for each download), you will need the script which will be invoked on the installNotifyURI to process the status code. The following sample code is the relevant part of a script which could be used to process the status code:

$status_code = substr(trim(file_get_contents("php://input")), 0, 3);
switch($status_code) {
	case '900':		// Success!! Do whatever you need when it was correctly downloaded
	default:		// Download failed, do whatever you need to do in case of failure

For a complete description of this technology and the Download Descriptor itself, I highly recommend reading the OMA Download OTA Specification documents.

Appendix A: Streaming for Apple iPhone

Apple iPhone uses HTTP byte-ranges for requesting audio and video files. First, the Safari Web Browser requests the content, and if it's an audio or video file it opens it's media player. The media player then requests the first 2 bytes of the content, to ensure that the Webserver supports byte-range requests. Then, if it supports them, the iPhone's media player requests the rest of the content by byte-ranges and plays it.

Thomas Thomassen has done a great job on his PHP Resumable Download Server, providing working PHP code which supports byte-range downloads.

The following sample code is the complete version for the one from the first part of the article, but including byte-range support by using the rangeDownload() function when the $_SERVER['HTTP_RANGE'] header is present on the device's HTTP request. The rangeDownload() function is an exact copy&paste from Thomas Thomassen's code (only the relevant part).

if (is_file($file)) {
	header("Content-type: $mime_type");
	if (isset($_SERVER['HTTP_RANGE']))  { // do it for any device that supports byte-ranges not only iPhone
	else {
		header("Content-Length: ".filesize($file));
else {
	// some error...
function rangeDownload($file) {
	$fp = @fopen($file, 'rb');
	$size   = filesize($file); // File size
	$length = $size;           // Content length
	$start  = 0;               // Start byte
	$end    = $size - 1;       // End byte
	// Now that we've gotten so far without errors we send the accept range header
	/* At the moment we only support single ranges.
	 * Multiple ranges requires some more work to ensure it works correctly
	 * and comply with the spesifications:
	 * Multirange support annouces itself with:
	 * header('Accept-Ranges: bytes');
	 * Multirange content must be sent with multipart/byteranges mediatype,
	 * (mediatype = mimetype)
	 * as well as a boundry header to indicate the various chunks of data.
	header("Accept-Ranges: 0-$length");
	// header('Accept-Ranges: bytes');
	// multipart/byteranges
	if (isset($_SERVER['HTTP_RANGE'])) {
		$c_start = $start;
		$c_end   = $end;
		// Extract the range string
		list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
		// Make sure the client hasn't sent us a multibyte range
		if (strpos($range, ',') !== false) {
			// (?) Shoud this be issued here, or should the first
			// range be used? Or should the header be ignored and
			// we output the whole content?
			header('HTTP/1.1 416 Requested Range Not Satisfiable');
			header("Content-Range: bytes $start-$end/$size");
			// (?) Echo some info to the client?
		// If the range starts with an '-' we start from the beginning
		// If not, we forward the file pointer
		// And make sure to get the end byte if spesified
		if ($range0 == '-') {
			// The n-number of the last bytes is requested
			$c_start = $size - substr($range, 1);
		else {
			$range  = explode('-', $range);
			$c_start = $range[0];
			$c_end   = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
		/* Check the range and make sure it's treated according to the specs.
		// End bytes can not be larger than $end.
		$c_end = ($c_end > $end) ? $end : $c_end;
		// Validate the requested range and return an error if it's not correct.
		if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
			header('HTTP/1.1 416 Requested Range Not Satisfiable');
			header("Content-Range: bytes $start-$end/$size");
			// (?) Echo some info to the client?
		$start  = $c_start;
		$end    = $c_end;
		$length = $end - $start + 1; // Calculate new content length
		fseek($fp, $start);
		header('HTTP/1.1 206 Partial Content');
	// Notify the client the byte range we'll be outputting
	header("Content-Range: bytes $start-$end/$size");
	header("Content-Length: $length");
	// Start buffered download
	$buffer = 1024 * 8;
	while(!feof($fp) && ($p = ftell($fp)) <= $end) {
		if ($p + $buffer > $end) {
			// In case we're only outputtin a chunk, make sure we don't
			// read past the length
			$buffer = $end - $p + 1;
		set_time_limit(0); // Reset time limit for big files
		echo fread($fp, $buffer);
		flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.

Appendix B: General Content Descriptor (GCD)

In the mobile world, developers need not only to deal with devices peculiarities, but many times carriers and their networks have peculiarities too.

GCD is one of these cases. GCD is a technology specific to Sprint (US carrier). If you don't provide content for Sprint users, there's almost no chance that you will need to use it.
But it's good to know about it, I've also seen Iusacell from Mexico sell some Sprint phones, which need to make use of GCD.

A GCD is very similar to a JAD file. It's a simple text file that contains information which allows the download of content to Sprint PCS Vision-capable devices.
For downloading content to these devices you MUST use a GCD.

This is the content of a sample GCD file:

Content-Type: audio/midi
Content-Name: My Ringtone
Content-Version: 1.0
Content-Vendor: Company Name
Content-URL: http ://wap.mydomain.tld/download.php/123456abcdef/myringtone.mid
Content-Size: 3257

Each part of the file is very important for the phone in order to be able to download the content.

Name Definition
Content-Type The content's MIME-Type
Content-Name The name of the file, which will show up on the phone
Content-URL The URL where to download the content from
Content-Size The content size in bytes. Must be the exact size of the content file, or the phone will display an error when downloading it

It is very important that the GCD is created correctly, respecting the needed line-breaks. If not, it will not work on many devices.
Here is sample code for creating a GCD:

$gcd_file = $path_to_gcd_file.'/download.gcd';
$handle = fopen($gcd_file, 'a');
fwrite($handle, "\nContent-Type: $mime_type\n");
fwrite($handle, "Content-Name: $content_name\n");
fwrite($handle, "Content-Version: 1.0\n");
fwrite($handle, "Content-Vendor: Company Name\n");
fwrite($handle, "Content-URL: $download_url\n");
fwrite($handle, "Content-Size: $file_size\n");


Serving content for mobile devices is a whole different world compared to the desktop Web.

In this article I've presented some possible ways on how to deliver content successfully to mobile devices. Depending on your environment and/or needs you may want to serve content directly as filesystem files or by streaming them - this is something you must analyse, and determine what is the best option for your specific application. You may even have additional needs which are not described here, so you must think what are your needs and requirements and act according to them.

You may also want to use OMA Download or not, but I would recommend to use it. Despite the fact that it may not be mandatory on your scenario, it can be very helpful for support purposes and also for statistical reasons.

So take your time, think what's the best option for you, and happy content delivering!

Posted by juanin - 07 May 2008

juanin's picture

General Manager of Software Development, 3Cinteractive

Posted by gabe 7 years ago

Good work and keep it coming !

Excellent article Juan, well written.
I love clickable PHP amd MySQL function names in the code samples above. It's a great helper in case a reader doesn't know what the function actually does.

Posted by svo9712 7 years ago

Dear Juan

Great and very helpful article.

Muntasir Mamun Joarder
Sr. System Analyst
Product Service Innovation
Cell: +88 01817 183 021

Md. Muntasir Mamun Joarder Web Developer Ipswich City Council Australia Cell: +61 42 50 49 459

Posted by jeff brookes (not verified) 7 years ago

Good article and helpful source code.


Posted by Terren Suydam (not verified) 7 years ago


Great article, it's been very helpful. One question, though - the line that reads:

"Sending only the Content-Type and File-Length HTTP headers"

Should that say "Content-Length" instead of "File-Length"?


Posted by juanin 7 years ago

You're right Terren, it's a typo!

Should say "Content-Length", thanks for the correction!!! :)

Juan Nin - Manager of Software Development
3Cinteractive - Mobilizing Great Brands

Juan Nin - Manager of Software Development 3Cinteractive - Mobilizing Great Brands

Posted by sonyarianto 7 years ago

Great source code sample, well article :)

Sony AK
Mobitrek Indonesia

Posted by craignewton 7 years ago

Really great article, I love the fact that you can have a callback url in the OMA download, makes logging and stats updates really great to know if the guy actually got the content successfully.

I used content-disposition in one of my past projects, and it really screwed up downloads for smart phones in general, was unable to save the file to the phone.

Well done!

Craig Newton
Mobile J2ME Application Developer
Contributor to the J2ME Polish Project:

Craig Newton Mobile J2ME Application Developer Blog: Contributor to the J2ME Polish Project:

Posted by SOLVEASE 7 years ago

Great Article, well organized & really helpful.


Md. Mahabubur Rahman
Associate programmer
Wintel Ltd.
cell : +8801713181601

Posted by nerd79 7 years ago

This is really an awesome article. Information like this is extremely rare. Reading this has made me reinvent my approach towards delivering mobile content. I was wondering, can something like this be integrated into a standalone Flash application for mobile devices, whereas I "stream" a .3gp video clip? Http requests are possible within mobile flash applications, was hoping it was possible to somehow integrate this into a flash lite application. Thanks a lot


Posted by juanin 7 years ago

Hi Toni!!! Thanks for your feedback!!!
It's always great to hear that one has been able to be helpful. :)

Unfortunately I got no experience on Flash Lite, so I'm not sure about that...
Just as a guess, maybe the byte-ranges way of streaming (like for iPhone) works, but I can not really say... you could give it a try and then let us know!!! (or whatever other approach you find to work).

Best regards,

Juan Nin - Manager of Software Development
3Cinteractive - Mobilizing Great Brands

Juan Nin - Manager of Software Development 3Cinteractive - Mobilizing Great Brands

Posted by nerd79 7 years ago

Thanks! Will do.


Posted by nerd79 7 years ago

Hi Juan

I'm pretty new to php however, I've grasped the logic to the I-Phone code. My only question would be what do I send as a parameter for the $_SERVER['HTTP_RANGE'] header? What parameter/s would you suggest for initial testing purposes?

Also, I've come across some information that says in order to "stream" or progressive download 3pg or any video file from a php script, one would have to rearrange the MOOV ATOMs to the beginning of the file. Is there validity to this? Your feedback is most appreciated!


Posted by juanin 6 years ago

Hi Toni, sorry for the delay on replying.

Basically what you have to do is start sending the file content by chunks, follow the code and you should be able to reproduce it, it's quite commented also explaining what it does.

Regarding 3GP files I'm sorry but really don't know. so far I've only tested it with MP3 files.

Juan Nin - Manager of Software Development
3Cinteractive - Mobilizing Great Brands

Juan Nin - Manager of Software Development 3Cinteractive - Mobilizing Great Brands

Posted by chessdev 6 years ago


We're trying to get this php workaround to work, but are having problems. Are you available to help? Please email me -> chessdev[&}gm@il :) Paid help of course!



Posted by thomthom 6 years ago

Glad to see my PHP Download Server is of use for other people.
If anyone implement multi byte-range and would like to share back then drop me a message and I can add it to the download on my site.

-Thomas Thomassen

Posted by andrew_waters 5 years ago

I've just been trying to get content streaming to an iPad / iPhone via a PHP script and came across this script which seemed to fit my requirements nicely.

However, there's a typo on line 67 which I spent 3 hours looking for - so in case you've come here looking for a way to stream to iDevices then instead of using:

if ($range0 == '-') {


if ($range == '-') {

It may save you some heartache / hairpulling ;)

Posted by srk974 4 years ago

I try to integrate this code inside my web page to play video (mp4 files) on an ipad but I don't know how to use it.
Somebody can help me ?

I hosted my web server on my Synology NAS and I work with wordpress.

Posted by halsafar 3 years ago

The Range Download function seems to die if more than one client is accessing it simultaneously or if one browser loads the same video in two spots. Perhaps this is partly the browsers fault as well.

Basically if I setup two html5 video tags on the same page referencing the same video via a php page that ends up using range download I get very weird results. Only one will show up at first. It will play fine. If I try and jump to a new time it will stall and the second video will start playing from a random position. I'm having a lot of trouble trying to debug this. Any advice?

It appears to work fine if they both use the normal download where it just calls read file.