Friday, July 12, 2013

GridFS HTML5 Video Streaming

One of the projects I'm working on uses MongoDB's GridFS to store video files to be played online.  Due to the size of the files it's not possible to simply just load them in to RAM and send them to the client, they need to be be read and streamed out.   Also, in order to support seeking with HTML5 video, we need to account for range requests being sent to the server.

This code example should provide enough in order to see how the process works.  It attempts to handle errors, and range requests in a sane way.

 $this->mdb = connect_mongodb();  
 $this->gdb = $this->mdb->getGridFS();  
 $fh = $this->gdb->findOne(array('filename' => $filename));  
 if ($fh == null ) {  
   header("HTTP/1.1 404 Not Found");  
   return false;  
 }  
 $stream = $fh->getResource();  
 $length = $fh->file['length'];  
 // Check for range request  
 if (isset($_SERVER['HTTP_RANGE'])) {  
   $range = explode('=',$_SERVER['HTTP_RANGE'],2);  
   if (strtolower(trim($range[0])) != 'bytes') {  
     header('HTTP/1.1 416 Requested Range Not Satisfiable');  
     return false;  
   }  
   if (substr_count($range[1],',') >= 1) {  
     //TODO handle multi ranges  
   }  
   if ($range[1]{0} == '-') {  
     //range begins in reverse  
     $req_bytes = substr($range[1],1);  
     (int)$start = (int)$length - 1 - (int)$req_bytes;  
     (int)$end = (int)$length - 1;  
   }  
   if (is_numeric(substr($range[1],0,1)) === true) {  
     $dash_position = strpos($range[1], '-');  
     $str_length = strlen($range[1]);  
     if (($str_length - 1) == $dash_position) {  
       //Dash position on the end, so we want the remaining amount  
       $start = (int)$range[1];  
       $end = (int)$length - 1;  
     } else {  
       //There's something after the dash  
       $r_parts = explode('-',$range[1],2);  
       $start = (int)$r_parts[0];  
       $end = (int)$r_parts[1];  
     }  
   }  
   $c_length = $end - $start + 1;  
   $a = $end - $start;  
   header('HTTP/1.1 206 Partial Content');  
   header("Content-Range: bytes {$start}-{$end}/{$length}");  
   header("Content-Length: {$c_length}");  
   fseek($stream, $start);  
 } else {  
   header("Content-Length: " . $fh->file['length']);  
   $end = $length - 1;  
 }  
 header("Content-type: video/{$matches[3]}");  
 header("Content-Disposition: inline");  
 header("Accept-Ranges: bytes");  
 header("X-Accel-Buffering: no");  
 $out = 0;  
 $bytes = 8192;  
 ob_implicit_flush(true);  
 ob_end_flush();  
 set_time_limit(0);  
 do {  
   if ($end < $bytes) {  
     $bytes = $end + 1;  
   }  
   echo fread($stream, $bytes);  
   flush();  
   $out = $out + $bytes;  
   $end = $end - $bytes;  
   if ($end <= 0) {  
     break;  
   }  
   if (connection_status() != CONNECTION_NORMAL) {  
     break;  
   }  
 } while (true);  
 fclose($stream);  

While this example doesn't handle every possible combination of range requests, in my testing it handles every case I've come across.


No comments:

Post a Comment