I'm writing app in symfony2
and I have a problem with video streaming.
If one action takes a long time to execute - for example 1 minute
, the whole app is frozen (when opened in second tab) and must wait for end of that execution.
Where is the problem?
//EDIT
public function streamAction($fileName) {
$user = $this->get('security.context')->getToken()->getUser();
$request = $this->getRequest();
$uid = $request->get('uid') != 'null' ? $user->getId() : $request->get('uid');
$libPath = $this->_libPath('Users', 'uid' . str_pad($uid, 6, "0", STR_PAD_LEFT));
$file = pathinfo($fileName);
$fileName = $file['basename'];
$fileExt = $file['extension'];
$filePath = realpath($libPath . $fileName);
if (in_array($fileExt, $this->formats['video'])) {
$mime = 'video';
}
if (in_array($fileExt, $this->formats['audio'])) {
$mime = 'audio';
}
$mime .= '/' . $fileExt;
header("Accept-Ranges: bytes");
if (is_file($filePath)) {
header("Content-type: $mime");
if (isset($_SERVER['HTTP_RANGE'])) {
$fp = fopen($filePath, 'rb');
$size = filesize($filePath);
$length = $size;
$start = 0;
$end = $size - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
if ($range == '-') {
$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;
}
$c_end = ($c_end > $end) ? $end : $c_end;
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");
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
fseek($fp, $start);
header('HTTP/1.1 206 Partial Content');
header('ETag: "' . md5(microtime()) . '"');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: $length");
header('Connection: Close');
$buffer = 1024 * 8;
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0);
echo fread($fp, $buffer);
flush();
}
fclose($fp);
} else {
header("Content-Length: " . filesize($filePath));
readfile($filePath);
}
} else {
echo "error";
}
die();
}
Problem is not code because symfony2 block not only with streaming but with other long time actions like downloading files.
If you use an OOP framework such as Symfony, why do still use procedural methods instead of what Symfony has to offer ?
For instance the StreamedResponse
class.
I provided you a more OOP / Symfony oriented below,
However in case the provided Range is just -
, you currently reset the range start to... The filesize? I am pretty sure this is not what you should do. I think you should double-check it!
Here is a reviewed version of streamAction() :
use SplFileInfo;
use RuntimeException;
// Symfony >= 2.1
use Symfony\Component\HttpFoundation\StreamedResponse;
public function streamAction($fileName) {
$user = $this->getUser();
$request = $this->getRequest();
// Create the StreamedResponse object
$response = new StreamedResponse();
$uid = $request->get('uid') != 'null' ? $user->getId() : $request->get('uid');
$libPath = $this->_libPath('Users', 'uid' . str_pad($uid, 6, "0", STR_PAD_LEFT));
try {
$file = new SplFileObject($libPath . $fileName);
}
catch (RuntimeException $runtimeException) {
// The file cannot be opened (permissions?)
// throw new AnyCustomFileErrorException() maybe?
}
// Check file existence
if (!($file->isFile())) {
// The file does not exists, or is not a file.
throw $this->createNotFoundException('This file does not exists, or is not a valid file.');
}
// Retrieve file informations
$fileName = $file->getBasename();
$fileExt = $file->getExtension();
$filePath = $file->getRealPath();
$fileSize = $file->getSize();
// Guess MIME Type from file extension
if (in_array($fileExt, $this->formats['video'])) {
$mime = 'video';
} elseif (in_array($fileExt, $this->formats['audio'])) {
$mime = 'audio';
}
$mime .= '/' . $fileExt;
$response->headers->set('Accept-Ranges', 'bytes');
$response->headers->set('Content-Type', $mime);
// Prepare File Range to read [default to the whole file content]
$rangeMin = 0;
$rangeMax = $fileSize - 1;
$rangeStart = $rangeMin;
$rangeEnd = $rangeMax;
$httpRange = $request->server->get('HTTP_RANGE');
// If a Range is provided, check its validity
if ($httpRange) {
$isRangeSatisfiable = true;
if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $httpRange, $matches)) {
$rangeStart = intval($matches[1]);
if (!empty($matches[2])) {
$rangeEnd = intval($matches[2]);
}
} else {
// Requested HTTP-Range seems invalid.
$isRangeSatisfiable = false;
}
if ($rangeStart <= $rangeEnd) {
$length = $rangeEnd - $rangeStart + 1;
} else {
// Requested HTTP-Range seems invalid.
$isRangeSatisfiable = false;
}
if ($file->fseek($rangeStart) !== 0) {
// Could not seek the file to the requested range: it might be out-of-bound, or the file is corrupted?
// Assume the range is not satisfiable.
$isRangeSatisfiable = false;
// NB : You might also wish to throw an Exception here...
// Depending the server behaviour you want to set-up.
// throw new AnyCustomFileErrorException();
}
if ($isRangeSatisfiable) {
// Now the file is ready to be read...
// Set additional headers and status code.
// Symfony < 2.4
// $response->setStatusCode(206);
// Or using Symfony >= 2.4 constants
$response->setStatusCode(StreamedResponse::HTTP_PARTIAL_CONTENT);
$response->headers->set('Content-Range', sprintf('bytes %d/%d', $rangeStart - $rangeEnd, $fileSize));
$response->headers->set('Content-Length', $length);
$response->headers->set('Connection', 'Close');
} else {
$response = new Response();
// Symfony < 2.4
// $response->setStatusCode(416);
// Or using Symfony >= 2.4 constants
$response->setStatusCode(StreamedResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
$response->headers->set('Content-Range', sprintf('bytes */%d', $fileSize));
return $response;
}
} else {
// No range has been provided: the whole file content can be sent
$response->headers->set('Content-Length', $fileSize);
}
// At this step, the request headers are ready to be sent.
$response->prepare($request);
$response->sendHeaders();
// Prepare the StreamCallback
$response->setCallback(function () use ($file, $rangeEnd) {
$buffer = 1024 * 8;
while (!($file->eof()) && (($offset = $file->ftell()) < $rangeEnd)) {
set_time_limit(0);
if ($offset + $buffer > $rangeEnd) {
$buffer = $rangeEnd + 1 - $offset;
}
echo $file->fread($buffer);
}
// Close the file handler
$file = null;
});
// Then everything should be ready, we can send the Response content.
$response->sendContent();
// DO NOT RETURN $response !
// It has already been sent, both headers and body.
}
This happens because PHP locks session. Your first request will lock the session and subsequent requests with the same session (Your second tab) will block in session_start()
until the first request finishes. (Requests with the same session are finished in serial, one after the next.)
Check out this link for more information on session concurrency.