WWW FAQs: How do I keep people from copying the MP3s on my site?


2005-08-15: Most folks who sell access to music online, or distribute samples of their band's work, would love a way to copy protect their MP3 files. Unfortunately there is no surefire way to prevent users from stealing your MP3 and other audio files. However, there are steps you can take to make it harder. And the tougher it is to steal something, the more aware you are that you are stealing. Yes, thieves have lockpicks, but locking the door helps to keep honest people honest.

The major legitimate music-buying websites, like iTunes, use "DRM" (Digital Rights Management) schemes to prevent users from sharing copies of their songs with other people. These systems encrypt every purchased song with a digital signature that only the purchasing user's computer is set up to decrypt. But these schemes are far from perfect. The iTunes DRM system, which is currently the best available, has several limitations. Users can burn songs to a CD, and then convert those tracks to MP3 files... with some loss of quality. Users can also, however, convert those tracks to unencrypted AAC files similar to Apple's encrypted AAC files, without loss of quality. And the "fake audio driver" and line-out-jack methods, of course, work with iTunes just as they do with MP3s.

So: nothing's perfect. But what can we do to make copying MP3s tougher? There are several techniques that help make this difficult, especially for naive users:

1. Do not allow MP3s to be fetched from an ordinary website folder. This prevents users from easily "mirroring" the entire directory.

2. Allow MP3s to be accessed only in response to filling out a form.

3. Don't create permanent links to single songs that users could copy and paste and send to others.

4. Always play the song in an embedded player in the web page. When doing so, add the kioskmode="true" attribute to the <embed> element, which asks the player to disable any "save file" features it might otherwise offer.

5. Mark the MP3 file with a special comment containing the user's IP address and the time and date. If your file does show up on a public file sharing system, you can check for the comment and then pursue the issue by contacting the ISP that owns the IP address with a complaint about the illegal activities of the user who was assigned that IP address at that time. If you have a budget for such things, you can pursue legal action, beginning with a subpoena to obtain the identity of that user from the ISP in question. Of course, it is possible for users to remove comments from MP3 files. But most users will not realize the incriminating comment is there.

6. Embed the MP3 file in a Flash file. Many users don't have the skills to save a copy of a Flash file for their own use or extract an MP3 audio stream from a Flash SWF file.

None of these tactics are perfect. But all of them slow thieves down. The more we use at once, the better! In this article we'll use techniques #1-5. (The use of an embedded player (#4) is roughly equivalent to #6, because most embedded players don't have a "Save" feature.)

But how do we accomplish these things? Most of these steps require a bit of PHP programming. So we'll do that. (Yes, you could also use Perl/CGI or ASP.NET. But we'll present a PHP solution here.)

We'll use two PHP pages, playmenu.php and playonce.php, to do the job. playmenu.php allows the user to pick a song from a menu, and playonce.php takes care of delivering that song inside an embedded player.

While we're at it, we'll display the song title, album name and band name, when available, fetching that information from the "ID3 tags" in the MP3 files. And we'll make the song "stream," playing back as it is downloaded, to minimize the user's wait time.

This is an advanced technique. To understand it better, I suggest that you master the following related articles first: How do I embed sound and music in a web page, How do I play streaming audio, and How do I play songs in a random order on my web page? These articles demonstrate the basic ideas used in this article. If you have trouble making this article's techniques work for you, make sure you master those more basic ideas first.

Follow these steps to install our "poor man's DRM" system:

1. Copy the two PHP scripts provided at the end of this article, playmenu.php and playonce.php, to an appropriate place on your website.

2. At the top of both scripts, you will see a setting like this:

$playOnceUrl = "http://www.example.com/playonce.php";

Change this setting to the URL where you have installed playonce.php.

3. At the top of both scripts, you will see a setting like this:

$mp3Dir = "/home/myhostingaccount/mp3s/";

Create a folder within your web hosting account, but NOT inside your document root (also known as a web folder), and place your MP3 files in that folder.

4. Change the $mp3Dir setting in both scripts to point to the new folder you created. If you created /home/myhostingaccount/mp3s/, then that is the setting you should use here. This is not a URL, it is a file path on your web server.

3. At the top of both scripts, you will see a setting like this:

$tmpDir = "/home/myhostingaccount/tmp/";

Create a folder called tmp within your web hosting account, but NOT inside your document root (also known as a web folder). This will be a folder to temporarily hold information about songs that users are in the process of downloading. These temporary files will hold the actual MP3 filename, as well as the song title, album and artist's name.

4. Change the $tmpDir setting in both scripts to point to the new folder you created. If you created /home/myhostingaccount/tmp/, then that is the setting you should use here. This is not a URL, it is a file path on your web server.

5. Access the playmenu.php script. If you installed it in the web folder (or "document root") of www.example.com, then the URL to access will be:

www.example.com/playmenu.php

If you followed steps 1-4 correctly and your web host supports PHP, then you will see a list of MP3s by title, band and artist (or filename, when these are not available). Pick a song from this list and click "Play Now" to listen. Then try copying and pasting the URL you see on the song player's page to a new browser window. The song won't play. Users can only play songs by accessing the menu on your site. And since embedded players don't provide an easy way to copy song files, copying the song is fairly difficult. If the user does use more sophisticated methods to copy the song, there will be a comment field added to the song giving their IP address and the date and time they downloaded it. If the user doesn't realize the significance of this information and remove it, and the song appears on a public file sharing system, you'll be in a position to track them down.

This is not a foolproof copy protection system for MP3s! It does not even come close! I'll say it again: there is no "magic" way to prevent users from copying your song files. Smart users may figure out how to bypass this system by using an embedded "player" that just saves files, or by using a specialized web-browsing script written in PHP or Perl that jumps through all of the right hoops. The user could then remove the IP and date/time stamp using an ID3 tag editor. Also, the user could use special audio driver software that looks like a speaker system to Windows Media Player or QuickTime Player, but also saves an MP3 stream -- at some cost in sound quality. A truly desperate user might simply hook up the "line out" jack of their computer to another computer and record to CD or MP3 that way... with a considerable loss of quality, of course.

In other words, don't even think about complaining to us when your songs get copied anyway.

However, this system does make copying songs harder. And that's a useful thing for those who just want to keep honest users honest.


playmenu.php


<?php
  # ABSOLUTELY NO WHITESPACE BEFORE THE ABOVE LINE!

  #CHANGE THIS to suit YOUR site! Do NOT put your mp3s folder
  #inside your website folder. This must be a FILE PATH ON YOUR
  #SERVER, where you have placed your actual MP3s. NOT a URL.
  #The / at the end is REQUIRED.
  $mp3Dir = "/home/webmail/mp3s/";
  $tmpDir = "/home/webmail/tmp/";
  #CHANGE THIS to suit where YOU installed playonce.php!
  $playOnceUrl = "http://mail.boutell.com/faqexperiments/fake-drm/playonce.php";
  #300 seconds (5 minutes) is a long life for one of our
  #temporary files, because they are only needed long enough
  #for the browser to fetch the player page, the playlist, and
  #the very start of the actual MP3. They don't have to be around
  # for the entiredownload.
  $maximumTmpAge = 300;  
  if ($_POST['play']) {
    play();
  }
  # Otherwise output a song selection page  
?>  
<html>
<head>
<title>Select A Song</title>
</head>
<body>
<h1>Select A Song</h1>
<b>WARNING:</b> downloads are for your personal use only. Your IP address and
the time of your download will be recorded in the song file. We can and
will identify and pursue legal remedies against those who distribute our
files through file sharing services.
<p>
<div align="center">
<form method="POST" action="playmenu.php">
<select name="mp3" size="10">
<?

if (!is_dir($mp3Dir)) {
  die("mp3Dir is not set correctly.");
}

$dir = opendir($mp3Dir);
if ($dir) {
  while (1) {
    $file = readdir($dir);
    if ($file == false) {
      break;
    }  
    if (!preg_match("/.mp3$/i", $file)) {
      continue;
    }
    # We found an MP3 file. Yank out the ID3v1.x tag
    # and find the title, artist and album to make
    # things pretty.
    $info = getInfo($file);
    # We show the title, artist and album here,
    # you can change this if you wish.
    $label = $info['artist'] . " " . $info['title'] .
      " ( " . $info['album'] . " )";
    # Don't allow funny business in ID3 tags to creep in and mess
    # up our layout. Replace anything that might alter the HTML.
    $label = preg_replace("/[\<\>\&\"]/", " ", $label);
    echo "<option value=\"$file\">$label</option>\n";
  }
}

function getInfo($file)
{
  global $mp3Dir;
  $handle = fopen($mp3Dir . $file, "rb");
  if (!$handle) {
    die("Can't open $file");
  }
  fseek($handle, -128, SEEK_END);
  $id = fread($handle, 128);
  $tag = substr($id, 0, 3);
  if ($tag != "TAG") {
    $title = $file;
  } else {
    $title = substr($id, 3, 30);
    $artist = substr($id, 33, 30);
    $album = substr($id, 63, 30);
    $title = trim($title);
    $artist = trim($artist);
    $album = trim($album);
  }
  $info['artist'] = $artist;
  $info['title'] = $title;
  $info['album'] = $album;
  return $info;
}

?>
</select>
<p>
<input type="submit" name="play" value="Play Now">
</form>
</div>
</body>
</html>

<?php
function play()
{
  global $playOnceUrl;
  global $tmpDir;
  $mp3 = $_POST['mp3'];
  if (!preg_match("/^[\w\-\+\.\@\ ]+$/", $mp3)) {
    # Dangerous characters in filename, reject it
    die("Bad filename $mp3");
  }
  $info = getInfo($mp3);
  if (!$info) {
    die("Bad filename, no info: $mp3");
  }
  # Needed for PHP versions OLDER than 4.2.0 only.
  # If your host still has PHP older than 4.2.0, shame on them.
  # Find a better web host.
  srand(makeSeed());

  # Clean up leftover temporary files from users who did not
  # have correctly configured audio players. If we don't do this,
  # the tmp directory will become cluttered with garbage.
  cleanupTemporaryFiles();

  # Generate a unique ID for this song download.
  $tmp = rand(0, 1 << 30);
  $handle = fopen("$tmpDir$tmp", "w");
  fwrite($handle, "$mp3\n");
  fwrite($handle, $info['artist'] . "\n");
  fwrite($handle, $info['title'] . "\n");
  fwrite($handle, $info['album'] . "\n");
  fclose($handle);
  # Now redirect to the playonce.php script which will
  # take advantage of this file (once to display a player,
  # a second time to generate a playlist file, and a final
  # time to deliver the actual IP-and-date-stamped MP3).
  header("Location: " . $playOnceUrl . "/" . $tmp . ".html");
}

function makeSeed()
{
  list($usec, $sec) = explode(' ', microtime());
  return (float) $sec + ((float) $usec * 100000);
}

function cleanupTemporaryFiles()
{
  global $maximumTmpAge;
  global $tmpDir;
  # Scan the temporary directory for old files and
  # clean them up.
  $dir = opendir($tmpDir);
  if (!$dir) {
    return;
  }
  while (1) {
    $file = readdir($dir);
    if ($file == false) {
      break;
    }  
    if (!preg_match("/^\d+$/", $file)) {
      continue;
    }
    $fileinfo = stat("$tmpDir$file");
    $now = time();
    $when = $fileinfo['mtime'];
    if ($now - $when > $maximumTmpAge) {
      unlink("$tmpDir$file");
    }  
  }
}
?>


playonce.php


<?php
#There can be ABSOLUTELY NOTHING before the <?php at the top of this file!
#Otherwise we can't choose to output an MP3 or playlist instead of HTML
#where appropriate.

$mp3Dir = "/home/myhostingaccount/mp3s/";
$tmpDir = "/home/myhostingaccount/tmp/";

#Change this to suit where YOU installed this page!
$playOnceUrl = "http://www.example.com/playonce.php";

if (preg_match("/(\d+)\.m3u$/", $_SERVER['PATH_INFO'], $matches)) {
  $tmp = $matches[1];
  playlist($tmp);
} elseif (preg_match("/(\d+)\.mp3$/", $_SERVER['PATH_INFO'], $matches)) {
  $tmp = $matches[1];
  mp3($tmp);
} elseif (preg_match("/(\d+)\.html$/", $_SERVER['PATH_INFO'], $matches)) {
  $tmp = $matches[1];
}

if (!$tmp) {
  die("Bad temporary ID #1");
}

$name = file($tmpDir . $tmp);
if (!$name[0]) {
  die("Bad temporary ID (no name)");
}
$artist = $name[1];
$title = $name[2];
$album = $name[3];

$script = $_SERVER['SCRIPT_NAME'] . "/" . $tmp . ".m3u";
#Now output the player HTML
?>

<html>
<head>
<title><?php echo "$artist"?>: <? echo "$title"?> (<? echo "$album"?>)</title>
</head>
<body>
<div align="center">
<b><?php echo "$artist"?></b><br>
<b><?php echo "$title"?></b><br>
<b><i><?php echo "$album"?></i></b><br>
<embed src="<?php echo "$script"?>"
  autostart="true"
  kioskmode="true"
  type="audio/mpeg"
  width="320"
  height="240"
  loop="true"/>
</div>
</body>
</html>

<?php
function mp3($tmp)
{
  global $mp3Dir;
  global $tmpDir;
  $name = file($tmpDir . $tmp);
  if (!$name[0]) {
    die("Bad temporary ID (no name in mp3)");
  }
  # Remove the temporary ID files so that they
  # can't be used repeatedly to easily mirror the site
  unlink($tmpDir . $tmp);
  header("Content-type: audio/mpeg");
  $mp3 = $name[0];
  $mp3 = trim($mp3);
  #Validate this for safety! Check for any
  #characters which are not A-Z, a-z, 0-9, . or _
  if (!preg_match("/^[\w\-\+\.\@\ ]+$/", $mp3)) {
    die("Unsafe mp3 filename parameter.");
  }
  $filename = $mp3Dir . $mp3;
  $handle = fopen($filename, "rb");
  if (!$handle) {
    die("No such mp3 file.");
  }

  # First read everything EXCEPT the id3v1.x tag
  # at the end.

  $size = filesize($filename);
  $id3TagSize = 128;
  $limit = $size - $id3TagSize;
  $pos = 0;
  while ($pos < $limit) {
   $chunk = 8192;
   if ($pos + $chunk > $limit) {
   $chunk = $limit - $pos;
   }
   $data = fread($handle, $chunk);
   print $data;
   $pos += $chunk;
  }

  #Now grab the last 128 bytes, which should be the ID3 tag,
  #and rewrite the comment field before sending it to the
  #browser. If we don't see an ID3 tag signature, output
  #what we did grab, which will be the tail end of the audio,
  #and then invent our own ID3 tag.

  $id = fread($handle, $id3TagSize);

  if (substr($id, 0, 3) != "TAG") {
    # Not really an ID3 tag, so write
    # out the last of the audio data and
    # invent our own ID3 tag
    print $id;
    # Now make an empty ID3 tag to append
    $id = pack("a128", "TAG");
  }

  #Record the IP address and time of download in the actual MP3 file,
  #as a comment. When you find your files on someone else's site you
  #can then determine when they were stolen and from what IP address.
  #That information can be used to pursue legal remedies, beginning by
  #obtaining the identity of the original downloader from their ISP
  #using this information. Note that the time logged in the file is
  #always GMT.

  $comment = $_SERVER['REMOTE_ADDR'] .
    " " . gmdate("Y-m-d h:i:sa", time());

  $newid = substr($id, 0, 97) . pack("a29", $comment) .
   substr($id, 126, 2);
  print $newid;
  fclose($handle);
  # We exit now to avoid writing newlines and other junk spaces
  # after the end of the PHP code as part of the MP3, which would
  # ruin our ID3 tag.
  exit(0);
}

function playlist($tmp)
{
  global $playOnceUrl;
  header("Content-type: audio/mpeg");
  $path = "http://" . $_SERVER['SERVER_NAME'] .
    ":" . $_SERVER['SERVER_PORT'] . $_SERVER['SCRIPT_NAME'];
  print "$playOnceUrl/$tmp" . ".mp3";
  # Don't let other junk appear at the end.
  exit(0);
}

?>


And that's it! This article does not explain every gory detail of how the scripts work. You can learn quite a bit more about what's going on by reading these articles: How do I embed sound and music in a web page, How do I play streaming audio, and How do I play songs in a random order on my web page? To understand the PHP code in more detail, you may wish to consult www.php.net, where you can easily look up any PHP function to see what's really going on "under the hood." Those who wish to understand how playonce.php modifies the ID3 tag to add a comment with the customer's IP address will want to check out Predrag Supurovic's unofficial MPEG audio format specification pages.

Legal Note: yes, you may use sample HTML, Javascript, PHP and other code presented above in your own projects. You may not reproduce large portions of the text of the article without our express permission.

Got a LiveJournal account? Keep up with the latest articles in this FAQ by adding our syndicated feed to your friends list!