I have an ios app with laravel (lumen) on the server side. I am trying to play the videos in the server, on application.
I am using a Player that plays videos with a direct link (e.g vine video link), however when I save the same vine video on my local server, the application doesn't play the video. In fact, when I try the video with my api route, surprisingly it plays the video on Chrome! But on the application end, I receive error:
The server is not correctly configured - 12939
(Please note that if I copy the same mp4 file into the xCode project, add it on 'copy bundle resources', and try with fileWithPath, it works. So I believe it's definitely caused by the server, not vidoo file/codec. )
My route: $app->get('/player/{filename}', 'PlayerController#show');
Methods:
public function show ($filename)
{
$this->playVideo($filename, 'recordings');
}
public function playVideo($filename, $showType)
{
if (file_exists("../uploads/" . $showType . "/" . $filename)) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, "../uploads/" . $showType . "/" . $filename);
header("Content-Type: " . $type);
readfile("../uploads/" . $showType . "/" . $filename);
}
}
To recap my problem, the video is playing on Chrome but receiving '12939' -
'Server is not correctly configured' on the mobile app.
Edit:
I tried using this as mentioned in the Apple Documentations:
curl --range 0-99 http://myapi.dev/test.mp4 -o /dev/null
However the documentation says:
"If the tool reports that it downloaded 100 bytes, the media server correctly handled the byte-range request. If it downloads the entire file, you may need to update the media server."
I received 100% and it downloaded the whole file for me, so I believe this is my problem. But I am not sure how to overcome this issue? What am I doing wrong? What should I do?
To wrap up, this solved my problem:
Placed this to VideoController:
public function streamVideo()
{
$video_path = 'somedirectory/somefile.mp4';
$stream = new VideoStream($video_path);
$stream->start();
}
and then created a file in app > helpers > VideoStream.php:
<?php
{
private $path = "";
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
function __construct($filePath)
{
$this->path = $filePath;
}
/**
* * Open stream
* */
private function open()
{
if (!($this->stream = fopen($this->path, 'rb'))) {
die('Could not open stream for reading');
}
}
/**
* * Set proper header to serve the video content
* */
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, public");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
header("Last-Modified: ".gmdate('D, d M Y H:i:s', #filemtime($this->path)) . ' GMT' );
$this->start = 0;
$this->size = filesize($this->path);
$this->end = $this->size - 1;
header("Accept-Ranges: 0-".$this->end);
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $this->start;
$c_end = $this->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 $this->start-$this->end/$this->size");
exit;
}
if ($range == '-') {
$c_start = $this->size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $this->end) ? $this->end : $c_end;
if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
$this->start = $c_start;
$this->end = $c_end;
$length = $this->end - $this->start + 1;
fseek($this->stream, $this->start);
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/".$this->size);
}
else
{
header("Content-Length: ".$this->size);
}
}
/**
* * close curretly opened stream
* */
private function end()
{
fclose($this->stream);
exit;
}
/**
* * perform the streaming of calculated range
* */
private function stream()
{
$i = $this->start;
set_time_limit(0);
while(!feof($this->stream) && $i <= $this->end) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = fread($this->stream, $bytesToRead);
echo $data;
flush();
$i += $bytesToRead;
}
}
/**
* * Start streaming video content
* */
function start()
{
$this->open();
$this->setHeader();
$this->stream();
$this->end();
}
}
Source: http://laravel.io/forum/10-06-2014-streaming-video-files-with-laravel
I ran into a problem similar to this one, but your case is going to take a bit more configuration.
The headers for iOS need to be set appropriately using range requests, and the only way I was able to do that for all browsers was by reworking this gist to what I needed.
In my case, I did something like this in Laravel:
//Video controller
...
public function showVideo($id){
$video = Video::find($id);
return $this->videoService->stream($video);
}
//Video Service
namespace App\Services\VideoService;
use Illuminate\Routing\ResponseFactory as Response;
use App\Models\Video;
class VideoService implements VideoServiceInterface
{
protected $response;
protected $video;
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
public function __construct(Response $response){
$this->response = $response;
}
public function stream(Video $video){
$this->video = $video;
return $this->response->stream(function(){
$this->start();
});
}
//Implement the rest of the gist here, renaming where appropriate....
}
Problem is, you're using Lumen which does not support a stream method on the response factory. What you'll need to do is rework the code I gave you to include Symfony's StreamedResponse Object.
If you look at how Laravel does it, you can probably do something like this:
//Video Service
namespace App\Services\VideoService;
use Symfony\Component\HttpFoundation\StreamedResponse as Response;
use App\Models\Video;
class VideoService implements VideoServiceInterface
{
protected $video;
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
public function stream(Video $video){
$this->video = $video;
return new Response(function(){
$this->start();
});
}
//Implement the rest of the gist here, renaming where appropriate....
}
This isn't exact, and it won't work for you out of the box. But this should give you all of the components you need to register your own Service Provider and apply this appropriately to your own use-case.
Good luck.
Related
I'm using a class for video stream:
<?php
/**
* Description of VideoStream
*
* #author Rana
* #link http://codesamplez.com/programming/php-html5-video-streaming-tutorial
*/
class VideoStream
{
private $path = "";
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
function __construct($filePath)
{
$this->path = $filePath;
}
/**
* Open stream
*/
private function open()
{
if (!($this->stream = fopen($this->path, 'rb'))) {
die('Could not open stream for reading');
}
}
/**
* Set proper header to serve the video content
*/
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, public");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
header("Last-Modified: ".gmdate('D, d M Y H:i:s', #filemtime($this->path)) . ' GMT' );
$this->start = 0;
$this->size = filesize($this->path);
$this->end = $this->size - 1;
header("Accept-Ranges: 0-".$this->end);
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $this->start;
$c_end = $this->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 $this->start-$this->end/$this->size");
exit;
}
if ($range == '-') {
$c_start = $this->size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $this->end) ? $this->end : $c_end;
if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
$this->start = $c_start;
$this->end = $c_end;
$length = $this->end - $this->start + 1;
fseek($this->stream, $this->start);
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/".$this->size);
}
else
{
header("Content-Length: ".$this->size);
}
}
/**
* close curretly opened stream
*/
private function end()
{
fclose($this->stream);
exit;
}
/**
* perform the streaming of calculated range
*/
private function stream()
{
$i = $this->start;
set_time_limit(0);
while(!feof($this->stream) && $i <= $this->end) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = fread($this->stream, $bytesToRead);
echo $data;
flush();
$i += $bytesToRead;
}
}
/**
* Start streaming video content
*/
function start()
{
$this->open();
$this->setHeader();
$this->stream();
$this->end();
}
}
I can call this class by doing:
include_once 'videostream.php';
$stream = new VideoStream("video.mp4");
$stream->start();
This code works with local videos, but the problem is: When I try to open a external video like this:
$stream = new VideoStream("https://www.w3schools.com/html/mov_bbb.ogg");
The vídeo don't open and I don't receive any error messages? Is a fopen() problem? Is a server problem? I'm using xampp v3.3.0.
When I see Network tab in Google Chrome, I receive two headers and one of them is pending.
UPDATE 1 (Partial Solution)
After performing some tests I made some modifications to the code and I was able to open the video using an external URL, follow the corrections below:
1.The video that I wanna play has HTTPS (https://www.w3schools.com/html/mov_bbb.ogg) and not HTTP format, so you must change all
headers that was HTTP/1.1 to HTTPS/1.1:
header('HTTPS/1.1 416 Requested Range Not Satisfiable');
header('HTTPS/1.1 206 Partial Content');
2.The command filesize does not work when it comes to an external URL, so we need to create a function capable of getting the size of the file we want:
function getContentLengthFromURL($url){
$arrayHeaders = get_headers($url,true);
if(isset($arrayHeaders['Content-Length'])){
if(is_numeric($arrayHeaders['Content-Length']) && $arrayHeaders['Content-Length'] > 0){
return $arrayHeaders['Content-Length'];
}
}
return null;
}
After that we need to update setHeader() function:
//$this->size = filesize($this->path);
$this->size = $this->getContentLengthFromURL($this->path);
3.We need to comment out all the lines that specify the Content-Range: and Fseek()
//header("Content-Range: bytes $this->start-$this->end/$this->size");
//fseek($this->stream, $this->start);
Amazing, now the HTML5 player is able to play the video, the only problem is that you are still unable to change the time of the video.
In this case I can't jump forward or back my video.
UPDATE 2 (THE SOLUTION)
After several attempts I finally got a solution to the code problem.
The problem was in the content-ranges and in the positioning of the headers.
By the way, I created a videoStream package using the new code, if you want an automated videoStream, just access this link.
I'm fairly new to PHP and definitely new to streaming video.
I have mp4 videos stored in a filestream column of my SQL SERVER, and I'd like to stream them to HTML5 video.
I've managed to use the following stream class ( http://codesamplez.com/programming/php-html5-video-streaming-tutorial ) by providing it a temporary file via 'file_put_contents($filePath, $queryresult),' but having to depend on writing and then cleaning up an intermediate file in the filesystem is problematic in the end.
Am I missing a key concept that would allow me to stream a file directly from the DB to the video "src" without having to write to a file in between?
Thanks in advance!
If you were creating temp files yourself and then deleting it. Then you can improve the process by using PHP's inbuilt
tempfile() which
Creates a temporary file with a unique name in read-write (w+) mode and returns a file handle .The file is automatically removed when closed (for example, by calling fclose(), or when there are no remaining references to the file handle returned by tmpfile()), or when the script ends.
private function open()
{
if ( !($this->stream = tmpfile()) ) {
fwrite($this->stream, $videodata);
rewind($this->stream);
die('Could not open stream for reading');
}
// now your temp file is ready to be read.
}
So now you have no responsibility of temp. deletion PHP will handle that for you.If you need to customize the temp. file name or get details you may use this http://php.net/manual/en/function.sys-get-temp-dir.php
If you don't want to use files at all and rather want to use purely in memory solution then you can try using memory streams
private function open()
{
if ( !($this->stream = fopen('php://memory', 'wb+') ) ) {
fwrite($this->stream, $videodata);
rewind($this->stream);
die('Could not open stream for reading');
}
// now your in-memory file is ready to be read.
}
You can fread, fwrite, file_get_contents on the memory stream or throw it over the network using the tcp streams. But I must say that the second solution is a bit memory intensive so not suitable for streaming huge files.
Writing a temporary file seems like a temporary workaround for me. As you are getting a stream from the database and you need to serve a stream to the client, there is no need to store data on the harddisk at all.
All you need to do is to replace the part of your linked example that reads from file (and echo's the binary content to the client) and make it read from sql instead.
In your linked example function stream(), replace this part
while(!feof($this->stream) && $i <= $this->end) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = fread($this->stream, $bytesToRead);
echo $data;
flush();
$i += $bytesToRead;
}
Replace by somthing like this:
/* Execute the query. */
$stmt = sqlsrv_query($conn, $tsql, $params);
if( $stmt === false )
{
echo "Error in statement execution.</br>";
die( print_r( sqlsrv_errors(), true));
}
/* Retrieve and display the data.
The return data is retrieved as a binary stream. */
if ( sqlsrv_fetch( $stmt ) )
{
$videostream = sqlsrv_get_field( $stmt, 0,
SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY));
//header("Content-Type: image/jpg");
fpassthru($videostream );
}
else
{
echo "Error in retrieving data.</br>";
die(print_r( sqlsrv_errors(), true));
}
For posterity, here's my final result, which seems to work well without needing temp files.
Not certain if it would make more sense to include the query into the class and just pass the $MediaFileID to the class, but this works as is, so for now I've left it.
The entire modified class is included with its original credit info:
function ExecutereadMediaSP($MediaFileID){
try{
$connection = ConnectToDB();
ini_set('memory_limit', '-1');
// logs basic info about media viewer. Mainly for a basic hit counter.
LogMediaRequest($connection, $MediaFileID);
//Selects binary data from SQL Server based on MediaFileID. Passes this to Stream Class. May be able to make further improvements later.
$sql = "SELECT ... data from FILESTREAM column based on file ID ...";
$rst = $connection->prepare($sql);
$rst->execute();
$rst->bindColumn(1, $filecontent, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY);
$row = $rst->fetch(PDO::FETCH_ASSOC);//sql can only return one row due to unique identifier
//Stream file
$stream = new VideoStream($filecontent);
$stream->start();
//Clean up.
$rst->closeCursor();
unset($rst);
$connection = null;
} catch (Exception $e) {
error_log("Error in getting video\n".$e->getMessage(),0);
}
}
/**
* VideoStream - PHP class that supports (adaptive) streaming of files
*
* #author Rana
* modified by HazCod to use stream_get_contents and correct session shutoff
* https://github.com/HazCod
* #link http://codesamplez.com/programming/php-html5-video-streaming-tutorial
*/
class VideoStream
{
private $path = "";
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
function __construct($filecontent)
{
try{
// Opens file handle resource to replace use of actual file in the file system.
$file_handle = fopen('php://memory', 'r+', false, stream_context_create());
// Writes data from SQL Query to file wrapper.
fwrite($file_handle, $filecontent);
// Moves pointer to beginning of file.
fseek($file_handle, 0);
// Gets info on the "file." Required to get filesize.
$fstat = array();
// gather statistics
$fstat = fstat($file_handle);
//Set File Size for Stream Class.
$this->size = $fstat['size'];
// Define Stream as "File."
$this->stream = $file_handle;
} catch (PDOException $e) {
error_log("Error in getting video\n".$e->getMessage(),0);
}
}
/**
* Set proper header to serve the video content
*/
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, public");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
// header("Last-Modified: ".gmdate('D, d M Y H:i:s', #filemtime($this->path)) . ' GMT' );
$this->start = 0;
$this->end = $this->size - 1;
// header("Accept-Ranges: 0-".$this->end);
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $this->start;
$c_end = $this->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 $this->start-$this->end/$this->size");
exit;
}
if ($range == '-') {
$c_start = $this->size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $this->end) ? $this->end : $c_end;
if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
$this->start = $c_start;
$this->end = $c_end;
$length = $this->end - $this->start + 1;
fseek($this->stream, $this->start);
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/".$this->size);
}
else
{
header("Content-Length: ".$this->size);
}
}
/**
* close curretly opened stream
*/
private function end()
{
fclose($this->stream);
exit;
}
/**
* perform the streaming of calculated range
*/
private function stream()
{
$i = $this->start;
set_time_limit(0);
while(!feof($this->stream) && $i <= $this->end && connection_aborted() == 0) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = stream_get_contents($this->stream, $bytesToRead);
echo $data;
flush();
$i += $bytesToRead;
}
}
/**
* Start streaming video content
*/
function start()
{
session_write_close(); //ensure our session is written away before streaming, else we cannot use it elsewhere
$this->setHeader();
$this->stream();
$this->end();
}
}
I have a 200MB file that I want to give to a user via download. However, since we want the user to only download this file once, we are doing this:
echo file_get_contents('http://some.secret.location.com/secretfolder/the_file.tar.gz');
to force a download. However, this means that the whole file has to be loaded in memory, which usually doesn't work. How can we stream this file to them, at some kb per chunk?
Try something like this (source http://teddy.fr/2007/11/28/how-serve-big-files-through-php/):
<?php
define('CHUNK_SIZE', 1024*1024); // Size (in bytes) of tiles chunk
// Read a file and display its content chunk by chunk
function readfile_chunked($filename, $retbytes = TRUE) {
$buffer = '';
$cnt = 0;
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, CHUNK_SIZE);
echo $buffer;
ob_flush();
flush();
if ($retbytes) {
$cnt += strlen($buffer);
}
}
$status = fclose($handle);
if ($retbytes && $status) {
return $cnt; // return num. bytes delivered like readfile() does.
}
return $status;
}
// Here goes your code for checking that the user is logged in
// ...
// ...
if ($logged_in) {
$filename = 'path/to/your/file';
$mimetype = 'mime/type';
header('Content-Type: '.$mimetype );
readfile_chunked($filename);
} else {
echo 'Tabatha says you haven\'t paid.';
}
?>
Use fpassthru(). As the name suggests, it doesn't read the entire file into memory prior to sending it, rather it outputs it straight to the client.
Modified from the example in the manual:
<?php
// the file you want to send
$path = "path/to/file";
// the file name of the download, change this if needed
$public_name = basename($path);
// get the file's mime type to send the correct content type header
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $path);
// send the headers
header("Content-Disposition: attachment; filename=$public_name;");
header("Content-Type: $mime_type");
header('Content-Length: ' . filesize($path));
// stream the file
$fp = fopen($path, 'rb');
fpassthru($fp);
exit;
If you would rather stream the content directly to the browser rather than a download (and if the content type is supported by the browser, such as video, audio, pdf etc) then remove the Content-Disposition header.
Take a look at the example from the manual page of fsockopen():
$fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
$out = "GET / HTTP/1.1\r\n";
$out .= "Host: www.example.com\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}
This will connect to www.example.com, send a request then get and echo the response in 128 byte chunks. You may want to make it more than 128 bytes.
I found this method in http://codesamplez.com/programming/php-html5-video-streaming-tutorial
And it works very well for me
<?php
class VideoStream
{
private $path = "";
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
function __construct($filePath)
{
$this->path = $filePath;
}
/**
* Open stream
*/
private function open()
{
if (!($this->stream = fopen($this->path, 'rb'))) {
die('Could not open stream for reading');
}
}
/**
* Set proper header to serve the video content
*/
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, public");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
header("Last-Modified: ".gmdate('D, d M Y H:i:s', #filemtime($this->path)) . ' GMT' );
$this->start = 0;
$this->size = filesize($this->path);
$this->end = $this->size - 1;
header("Accept-Ranges: 0-".$this->end);
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $this->start;
$c_end = $this->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 $this->start-$this->end/$this->size");
exit;
}
if ($range == '-') {
$c_start = $this->size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $this->end) ? $this->end : $c_end;
if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
$this->start = $c_start;
$this->end = $c_end;
$length = $this->end - $this->start + 1;
fseek($this->stream, $this->start);
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/".$this->size);
}
else
{
header("Content-Length: ".$this->size);
}
}
/**
* close curretly opened stream
*/
private function end()
{
fclose($this->stream);
exit;
}
/**
* perform the streaming of calculated range
*/
private function stream()
{
$i = $this->start;
set_time_limit(0);
while(!feof($this->stream) && $i <= $this->end) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = fread($this->stream, $bytesToRead);
echo $data;
flush();
$i += $bytesToRead;
}
}
/**
* Start streaming video content
*/
function start()
{
$this->open();
$this->setHeader();
$this->stream();
$this->end();
}
}
To use this class, you will have to write simple code like as below:
$stream = new VideoStream($filePath);
$stream->start();
I ran into this problem as well using readfile() to force a download. The memory problem lies not within readfile, rather with ouput buffering.
Just make sure you switch off output buffering before readfile, and the problem should be fixed.
if (ob_get_level()) ob_end_clean();
readfile($yourfile);
Works for files with a size much larger than the allocated memory limit.
I am trying to build a Media Server that will be used only on my own personal network. No outside access will be available, except for those devices that are connected via LAN or WiFi. I have ripped my entire DVD collection some SD, and some HD as to be able to play it via PS3, DVD Players, etc, without having to dig the disc out every time someone wants to watch something. Because I want to limit access to certain parts of the media, VLC, TVersity, Plex, etc don't work, and I want to custom tailor it with my own design without having to go to any outside source for processing and want it to be web based so it can be viewable in Safari or Chrome. All files are in .mp4 format, so transcoding is not necessary, or at least I don't think it is.
I have a working page that works on the machine that the XAMPP server is running on. The info is provided via a MySQL database and using PHP and HTML5 to play the video. I will be building a dedicated Linux server very soon, just trying to get it working. Files under 2GB work just fine on the same machine as XAMPP is installed (but NOT on devices that are connected via wifi or wire), but anything over 2GB just continuously say loading on all devices. Trying to view on iPad Air, iPhone6 but doesn't work.
The Streaming Code:
<?php
/**
* Description of VideoStream
*
* #author Rana
* #link http://codesamplez.com/programming/php-html5-video-streaming-tutorial
*/
class VideoStream
{
private $path = "";
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
function __construct($filePath)
{
$this->path = $filePath;
}
/**
* Open stream
*/
private function open()
{
if (!($this->stream = fopen($this->path, 'rb'))) {
die('Could not open stream for reading');
}
}
/**
* Set proper header to serve the video content
*/
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, public");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
header("Last-Modified: ".gmdate('D, d M Y H:i:s', #filemtime($this->path)) . ' GMT' );
$this->start = 0;
$this->size = filesize($this->path);
$this->end = $this->size - 1;
header("Accept-Ranges: 0-".$this->end);
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $this->start;
$c_end = $this->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 $this->start-$this->end/$this->size");
exit;
}
if ($range == '-') {
$c_start = $this->size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $this->end) ? $this->end : $c_end;
if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
$this->start = $c_start;
$this->end = $c_end;
$length = $this->end - $this->start + 1;
fseek($this->stream, $this->start);
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/".$this->size);
}
else
{
header("Content-Length: ".$this->size);
}
}
/**
* close curretly opened stream
*/
private function end()
{
fclose($this->stream);
exit;
}
/**
* perform the streaming of calculated range
*/
private function stream()
{
$i = $this->start;
set_time_limit(0);
while(!feof($this->stream) && $i <= $this->end) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = fread($this->stream, $bytesToRead);
echo $data;
flush();
$i += $bytesToRead;
}
}
/**
* Start streaming video content
*/
function start()
{
$this->open();
$this->setHeader();
$this->stream();
$this->end();
}
}
?>
The HTML Code:
<video width="320" height="240" controls><?php
$stream = new VideoStream($row_rsTitles['Category'].'/'.$row_rsTitles['Title'].'/'.$row_rsTitles['Filename']);
$stream->start();
?></video>
Any help would be appreciated. Many of hours, and trying a hundred different options with no success.
We are using a PHP scripting for tunnelling file downloads, since we don't want to expose the absolute path of downloadable file:
header("Content-Type: $ctype");
header("Content-Length: " . filesize($file));
header("Content-Disposition: attachment; filename=\"$fileName\"");
readfile($file);
Unfortunately we noticed that downloads passed through this script can't be resumed by the end user.
Is there any way to support resumable downloads with such a PHP-based solution?
The first thing you need to do is to send the Accept-Ranges: bytes header in all responses, to tell the client that you support partial content. Then, if request with a Range: bytes=x-y header is received (with x and y being numbers) you parse the range the client is requesting, open the file as usual, seek x bytes ahead and send the next y - x bytes. Also set the response to HTTP/1.0 206 Partial Content.
Without having tested anything, this could work, more or less:
$filesize = filesize($file);
$offset = 0;
$length = $filesize;
if ( isset($_SERVER['HTTP_RANGE']) ) {
// if the HTTP_RANGE header is set we're dealing with partial content
$partialContent = true;
// find the requested range
// this might be too simplistic, apparently the client can request
// multiple ranges, which can become pretty complex, so ignore it for now
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
$offset = intval($matches[1]);
$length = intval($matches[2]) - $offset;
} else {
$partialContent = false;
}
$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
$data = fread($file, $length);
fclose($file);
if ( $partialContent ) {
// output the right headers for partial content
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $filesize);
}
// output the regular HTTP headers
header('Content-Type: ' . $ctype);
header('Content-Length: ' . $filesize);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');
// don't forget to send the data too
print($data);
I may have missed something obvious, and I have most definitely ignored some potential sources of errors, but it should be a start.
There's a description of partial content here and I found some info on partial content on the documentation page for fread.
EDIT 2017/01 - I wrote a library to do this in PHP >=7.0 https://github.com/DaveRandom/Resume
EDIT 2016/02 - Code completely rewritten to a set of modular tools an an example usage, rather than a monolithic function. Corrections mentioned in comments below have been incorporated.
A tested, working solution (based heavily on Theo's answer above) which deals with resumable downloads, in a set of a few standalone tools. This code requires PHP 5.4 or later.
This solution can still only cope with one range per request, but under any circumstance with a standard browser that I can think of, this should not cause a problem.
<?php
/**
* Get the value of a header in the current request context
*
* #param string $name Name of the header
* #return string|null Returns null when the header was not sent or cannot be retrieved
*/
function get_request_header($name)
{
$name = strtoupper($name);
// IIS/Some Apache versions and configurations
if (isset($_SERVER['HTTP_' . $name])) {
return trim($_SERVER['HTTP_' . $name]);
}
// Various other SAPIs
foreach (apache_request_headers() as $header_name => $value) {
if (strtoupper($header_name) === $name) {
return trim($value);
}
}
return null;
}
class NonExistentFileException extends \RuntimeException {}
class UnreadableFileException extends \RuntimeException {}
class UnsatisfiableRangeException extends \RuntimeException {}
class InvalidRangeHeaderException extends \RuntimeException {}
class RangeHeader
{
/**
* The first byte in the file to send (0-indexed), a null value indicates the last
* $end bytes
*
* #var int|null
*/
private $firstByte;
/**
* The last byte in the file to send (0-indexed), a null value indicates $start to
* EOF
*
* #var int|null
*/
private $lastByte;
/**
* Create a new instance from a Range header string
*
* #param string $header
* #return RangeHeader
*/
public static function createFromHeaderString($header)
{
if ($header === null) {
return null;
}
if (!preg_match('/^\s*(\S+)\s*(\d*)\s*-\s*(\d*)\s*(?:,|$)/', $header, $info)) {
throw new InvalidRangeHeaderException('Invalid header format');
} else if (strtolower($info[1]) !== 'bytes') {
throw new InvalidRangeHeaderException('Unknown range unit: ' . $info[1]);
}
return new self(
$info[2] === '' ? null : $info[2],
$info[3] === '' ? null : $info[3]
);
}
/**
* #param int|null $firstByte
* #param int|null $lastByte
* #throws InvalidRangeHeaderException
*/
public function __construct($firstByte, $lastByte)
{
$this->firstByte = $firstByte === null ? $firstByte : (int)$firstByte;
$this->lastByte = $lastByte === null ? $lastByte : (int)$lastByte;
if ($this->firstByte === null && $this->lastByte === null) {
throw new InvalidRangeHeaderException(
'Both start and end position specifiers empty'
);
} else if ($this->firstByte < 0 || $this->lastByte < 0) {
throw new InvalidRangeHeaderException(
'Position specifiers cannot be negative'
);
} else if ($this->lastByte !== null && $this->lastByte < $this->firstByte) {
throw new InvalidRangeHeaderException(
'Last byte cannot be less than first byte'
);
}
}
/**
* Get the start position when this range is applied to a file of the specified size
*
* #param int $fileSize
* #return int
* #throws UnsatisfiableRangeException
*/
public function getStartPosition($fileSize)
{
$size = (int)$fileSize;
if ($this->firstByte === null) {
return ($size - 1) - $this->lastByte;
}
if ($size <= $this->firstByte) {
throw new UnsatisfiableRangeException(
'Start position is after the end of the file'
);
}
return $this->firstByte;
}
/**
* Get the end position when this range is applied to a file of the specified size
*
* #param int $fileSize
* #return int
* #throws UnsatisfiableRangeException
*/
public function getEndPosition($fileSize)
{
$size = (int)$fileSize;
if ($this->lastByte === null) {
return $size - 1;
}
if ($size <= $this->lastByte) {
throw new UnsatisfiableRangeException(
'End position is after the end of the file'
);
}
return $this->lastByte;
}
/**
* Get the length when this range is applied to a file of the specified size
*
* #param int $fileSize
* #return int
* #throws UnsatisfiableRangeException
*/
public function getLength($fileSize)
{
$size = (int)$fileSize;
return $this->getEndPosition($size) - $this->getStartPosition($size) + 1;
}
/**
* Get a Content-Range header corresponding to this Range and the specified file
* size
*
* #param int $fileSize
* #return string
*/
public function getContentRangeHeader($fileSize)
{
return 'bytes ' . $this->getStartPosition($fileSize) . '-'
. $this->getEndPosition($fileSize) . '/' . $fileSize;
}
}
class PartialFileServlet
{
/**
* The range header on which the data transmission will be based
*
* #var RangeHeader|null
*/
private $range;
/**
* #param RangeHeader $range Range header on which the transmission will be based
*/
public function __construct(RangeHeader $range = null)
{
$this->range = $range;
}
/**
* Send part of the data in a seekable stream resource to the output buffer
*
* #param resource $fp Stream resource to read data from
* #param int $start Position in the stream to start reading
* #param int $length Number of bytes to read
* #param int $chunkSize Maximum bytes to read from the file in a single operation
*/
private function sendDataRange($fp, $start, $length, $chunkSize = 8192)
{
if ($start > 0) {
fseek($fp, $start, SEEK_SET);
}
while ($length) {
$read = ($length > $chunkSize) ? $chunkSize : $length;
$length -= $read;
echo fread($fp, $read);
}
}
/**
* Send the headers that are included regardless of whether a range was requested
*
* #param string $fileName
* #param int $contentLength
* #param string $contentType
*/
private function sendDownloadHeaders($fileName, $contentLength, $contentType)
{
header('Content-Type: ' . $contentType);
header('Content-Length: ' . $contentLength);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');
}
/**
* Send data from a file based on the current Range header
*
* #param string $path Local file system path to serve
* #param string $contentType MIME type of the data stream
*/
public function sendFile($path, $contentType = 'application/octet-stream')
{
// Make sure the file exists and is a file, otherwise we are wasting our time
$localPath = realpath($path);
if ($localPath === false || !is_file($localPath)) {
throw new NonExistentFileException(
$path . ' does not exist or is not a file'
);
}
// Make sure we can open the file for reading
if (!$fp = fopen($localPath, 'r')) {
throw new UnreadableFileException(
'Failed to open ' . $localPath . ' for reading'
);
}
$fileSize = filesize($localPath);
if ($this->range == null) {
// No range requested, just send the whole file
header('HTTP/1.1 200 OK');
$this->sendDownloadHeaders(basename($localPath), $fileSize, $contentType);
fpassthru($fp);
} else {
// Send the request range
header('HTTP/1.1 206 Partial Content');
header('Content-Range: ' . $this->range->getContentRangeHeader($fileSize));
$this->sendDownloadHeaders(
basename($localPath),
$this->range->getLength($fileSize),
$contentType
);
$this->sendDataRange(
$fp,
$this->range->getStartPosition($fileSize),
$this->range->getLength($fileSize)
);
}
fclose($fp);
}
}
Example usage:
<?php
$path = '/local/path/to/file.ext';
$contentType = 'application/octet-stream';
// Avoid sending unexpected errors to the client - we should be serving a file,
// we don't want to corrupt the data we send
ini_set('display_errors', '0');
try {
$rangeHeader = RangeHeader::createFromHeaderString(get_request_header('Range'));
(new PartialFileServlet($rangeHeader))->sendFile($path, $contentType);
} catch (InvalidRangeHeaderException $e) {
header("HTTP/1.1 400 Bad Request");
} catch (UnsatisfiableRangeException $e) {
header("HTTP/1.1 416 Range Not Satisfiable");
} catch (NonExistentFileException $e) {
header("HTTP/1.1 404 Not Found");
} catch (UnreadableFileException $e) {
header("HTTP/1.1 500 Internal Server Error");
}
// It's usually a good idea to explicitly exit after sending a file to avoid sending any
// extra data on the end that might corrupt the file
exit;
Yes. Support byteranges. See RFC 2616 section 14.35 .
It basically means that you should read the Range header, and start serving the file from the specified offset.
This means that you can't use readfile(), since that serves the whole file. Instead, use fopen() first, then fseek() to the correct position, and then use fpassthru() to serve the file.
This works 100% super check it
I am using it and no problems any more.
/* Function: download with resume/speed/stream options */
/* List of File Types */
function fileTypes($extension){
$fileTypes['swf'] = 'application/x-shockwave-flash';
$fileTypes['pdf'] = 'application/pdf';
$fileTypes['exe'] = 'application/octet-stream';
$fileTypes['zip'] = 'application/zip';
$fileTypes['doc'] = 'application/msword';
$fileTypes['xls'] = 'application/vnd.ms-excel';
$fileTypes['ppt'] = 'application/vnd.ms-powerpoint';
$fileTypes['gif'] = 'image/gif';
$fileTypes['png'] = 'image/png';
$fileTypes['jpeg'] = 'image/jpg';
$fileTypes['jpg'] = 'image/jpg';
$fileTypes['rar'] = 'application/rar';
$fileTypes['ra'] = 'audio/x-pn-realaudio';
$fileTypes['ram'] = 'audio/x-pn-realaudio';
$fileTypes['ogg'] = 'audio/x-pn-realaudio';
$fileTypes['wav'] = 'video/x-msvideo';
$fileTypes['wmv'] = 'video/x-msvideo';
$fileTypes['avi'] = 'video/x-msvideo';
$fileTypes['asf'] = 'video/x-msvideo';
$fileTypes['divx'] = 'video/x-msvideo';
$fileTypes['mp3'] = 'audio/mpeg';
$fileTypes['mp4'] = 'audio/mpeg';
$fileTypes['mpeg'] = 'video/mpeg';
$fileTypes['mpg'] = 'video/mpeg';
$fileTypes['mpe'] = 'video/mpeg';
$fileTypes['mov'] = 'video/quicktime';
$fileTypes['swf'] = 'video/quicktime';
$fileTypes['3gp'] = 'video/quicktime';
$fileTypes['m4a'] = 'video/quicktime';
$fileTypes['aac'] = 'video/quicktime';
$fileTypes['m3u'] = 'video/quicktime';
return $fileTypes[$extention];
};
/*
Parameters: downloadFile(File Location, File Name,
max speed, is streaming
If streaming - videos will show as videos, images as images
instead of download prompt
*/
function downloadFile($fileLocation, $fileName, $maxSpeed = 100, $doStream = false) {
if (connection_status() != 0)
return(false);
// in some old versions this can be pereferable to get extention
// $extension = strtolower(end(explode('.', $fileName)));
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$contentType = fileTypes($extension);
header("Cache-Control: public");
header("Content-Transfer-Encoding: binary\n");
header('Content-Type: $contentType');
$contentDisposition = 'attachment';
if ($doStream == true) {
/* extensions to stream */
$array_listen = array('mp3', 'm3u', 'm4a', 'mid', 'ogg', 'ra', 'ram', 'wm',
'wav', 'wma', 'aac', '3gp', 'avi', 'mov', 'mp4', 'mpeg', 'mpg', 'swf', 'wmv', 'divx', 'asf');
if (in_array($extension, $array_listen)) {
$contentDisposition = 'inline';
}
}
if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE")) {
$fileName = preg_replace('/\./', '%2e', $fileName, substr_count($fileName, '.') - 1);
header("Content-Disposition: $contentDisposition;
filename=\"$fileName\"");
} else {
header("Content-Disposition: $contentDisposition;
filename=\"$fileName\"");
}
header("Accept-Ranges: bytes");
$range = 0;
$size = filesize($fileLocation);
if (isset($_SERVER['HTTP_RANGE'])) {
list($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
str_replace($range, "-", $range);
$size2 = $size - 1;
$new_length = $size - $range;
header("HTTP/1.1 206 Partial Content");
header("Content-Length: $new_length");
header("Content-Range: bytes $range$size2/$size");
} else {
$size2 = $size - 1;
header("Content-Range: bytes 0-$size2/$size");
header("Content-Length: " . $size);
}
if ($size == 0) {
die('Zero byte file! Aborting download');
}
set_magic_quotes_runtime(0);
$fp = fopen("$fileLocation", "rb");
fseek($fp, $range);
while (!feof($fp) and ( connection_status() == 0)) {
set_time_limit(0);
print(fread($fp, 1024 * $maxSpeed));
flush();
ob_flush();
sleep(1);
}
fclose($fp);
return((connection_status() == 0) and ! connection_aborted());
}
/* Implementation */
// downloadFile('path_to_file/1.mp3', '1.mp3', 1024, false);
A really nice way to solve this without having to "roll your own" PHP code is to use the mod_xsendfile Apache module. Then in PHP, you just set the appropriate headers. Apache gets to do its thing.
header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
If you're willing to install a new PECL module, the easiest way to support resumeable downloads with PHP is through http_send_file(), like this
<?php
http_send_content_disposition("document.pdf", true);
http_send_content_type("application/pdf");
http_throttle(0.1, 2048);
http_send_file("../report.pdf");
?>
source : http://www.php.net/manual/en/function.http-send-file.php
We use it to serve database-stored content and it works like a charm !
Yes, you can use the Range header for that. You need to give 3 more headers to the client for a full download:
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $fileSize);
header ("Content-Range: bytes 0-" . $fileSize - 1 . "/" . $fileSize . ";");
Than for an interrupted download you need to check the Range request header by:
$headers = getAllHeaders ();
$range = substr ($headers['Range'], '6');
And in this case don't forget to serve the content with 206 status code:
header ("HTTP/1.1 206 Partial content");
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $remaining_length);
header ("Content-Range: bytes " . $start . "-" . $to . "/" . $fileSize . ";");
You'll get the $start and $to variables from the request header, and use fseek() to seek to the correct position in the file.
The top answer has various bugs.
The major bug: It doesn't handle Range header correctly. bytes a-b should mean [a, b] instead of [a, b), and bytes a- is not handled.
The minor bug: It doesn't use buffer to handle output. This may consume too much memory and cause low speed for large files.
Here's my modified code:
// TODO: configurations here
$fileName = "File Name";
$file = "File Path";
$bufferSize = 2097152;
$filesize = filesize($file);
$offset = 0;
$length = $filesize;
if (isset($_SERVER['HTTP_RANGE'])) {
// if the HTTP_RANGE header is set we're dealing with partial content
// find the requested range
// this might be too simplistic, apparently the client can request
// multiple ranges, which can become pretty complex, so ignore it for now
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
$offset = intval($matches[1]);
$end = $matches[2] || $matches[2] === '0' ? intval($matches[2]) : $filesize - 1;
$length = $end + 1 - $offset;
// output the right headers for partial content
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $offset-$end/$filesize");
}
// output the regular HTTP headers
header('Content-Type: ' . mime_content_type($file));
header("Content-Length: $filesize");
header("Content-Disposition: attachment; filename=\"$fileName\"");
header('Accept-Ranges: bytes');
$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
// don't forget to send the data too
ini_set('memory_limit', '-1');
while ($length >= $bufferSize)
{
print(fread($file, $bufferSize));
$length -= $bufferSize;
}
if ($length) print(fread($file, $length));
fclose($file);
This worked very well for me: https://github.com/pomle/php-serveFilePartial
Small composer enabled class which works the same way as pecl http_send_file. This means support for resumable downloads and throttle. https://github.com/diversen/http-send-file
You could use the below code for byte range request support across any browser
<?php
$file = 'YouTube360p.mp4';
$fileLoc = $file;
$filesize = filesize($file);
$offset = 0;
$fileLength = $filesize;
$length = $filesize - 1;
if ( isset($_SERVER['HTTP_RANGE']) ) {
// if the HTTP_RANGE header is set we're dealing with partial content
$partialContent = true;
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
$offset = intval($matches[1]);
$tempLength = intval($matches[2]) - 0;
if($tempLength != 0)
{
$length = $tempLength;
}
$fileLength = ($length - $offset) + 1;
} else {
$partialContent = false;
$offset = $length;
}
$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
$data = fread($file, $length);
fclose($file);
if ( $partialContent ) {
// output the right headers for partial content
header('HTTP/1.1 206 Partial Content');
}
// output the regular HTTP headers
header('Content-Type: ' . mime_content_type($fileLoc));
header('Content-Length: ' . $fileLength);
header('Content-Disposition: inline; filename="' . $file . '"');
header('Accept-Ranges: bytes');
header('Content-Range: bytes ' . $offset . '-' . $length . '/' . $filesize);
// don't forget to send the data too
print($data);
?>
Resuming downloads in HTTP is done through the Range header. If the request contains a Range header, and if other indicators (e.g. If-Match, If-Unmodified-Since) indicate that the content hasn't changed since the download was started, you give a 206 response code (rather than 200), indicate the range of bytes you're returning in the Content-Range header, then provide that range in the response body.
I don't know how to do that in PHP, though.
Thanks Theo! your method did not directly work for streaming divx because i found the divx player was sending ranges like bytes=9932800-
but it showed me how to do it so thanks :D
if(isset($_SERVER['HTTP_RANGE']))
{
file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
I've created a library for serving files with support for conditional (don't download file again unless it has changed) and ranged (pause and resume download) requests. It even works with virtual file systems, such as Flysystem.
Check it out here: FileWaiter
Example usage:
use Stadly\FileWaiter\Adapter\Local;
use Stadly\FileWaiter\File;
use Stadly\FileWaiter\Waiter;
$streamFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible stream factory.
$file = new File(new Local('filename.txt', $streamFactory)); // Or another file adapter. See below.
$responseFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible response factory.
$waiter = new Waiter($file, $responseFactory);
$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); // Any PSR-7 compatible server request.
$response = $waiter->handle($request); // The response is created by the response factory.
$emitter = new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter(); // Any way of emitting PSR-7 responses.
$emitter->emit($response);
die();