How to Stream Large File from AWS S3 Bucket to Browser in Laravel

If you want to stream large files from AWS S3 bucket to your browser using Laravel then this article is for you.

If the file size is really big then you can't directly load it on your browser because it may impact your browser performance. So it's better that you can load your file by using stream instead of loading the whole file at once at your browser.

In this article, I will explain to you how easily you can stream the large file directly from the S3 bucket in your browser by using a simple class. To do this I have created a simple class by the name of “S3FileStream.php”. This class accepts a $filePath as the default parameter by using the constructor. You just need to pass the path of your AWS S3 bucket file as a parameter. In my case, I need to read the file from my S3 bucket “project/media/media.mp4”. 

So I just pass the path to the class while creating objects and call the output() function by using object like below-

$stream = new S3FileStream(“project/media/media.mp4”);
return $stream->output();
Below is the S3FileStream.php class file -
<?php
namespace App\Libraries;

use App\Exceptions\CustomException;
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;

class S3FileStream
{
private $adapter;

/**
* Name of adapter
*
* @var string
*/
private $adapterName;

/**
* Storage disk
*
* @var FilesystemAdapter
*/
private $disk;

/**
* @var int file end byte
*/
private $end;

/**
* @var string
*/
private $filePath;

/**
* Human-known filename
*
* @var string|null
*/
private $humanName;

/**
* @var bool storing if request is a range (or a full file)
*/
private $isRange = false;

/**
* @var int|null length of bytes requested
*/
private $length = null;

/**
* @var array
*/
private $returnHeaders = [];

/**
* @var int file size
*/
private $size;

/**
* @var int start byte
*/
private $start;

/**
* S3FileStream constructor.
* @param string $filePath
* @param string $adapter
* @param string $humanName
*/
public function __construct(string $filePath, string $adapter = 's3', ?string $humanName = null)
{
$this->filePath = ltrim($filePath, "/");
$this->adapterName = $adapter;
$this->disk = Storage::disk($this->adapterName);
$this->adapter = $this->disk->getAdapter();
$this->humanName = $humanName;
//Set to zero until setHeadersAndStream is called
$this->start = 0;
$this->size = 0;
$this->end = 0;
}

/**
* Output file to client.
*/
public function output()
{
return $this->setHeadersAndStream();
}

/**
* Output headers to client.
* @return Response|StreamedResponse
*/
protected function setHeadersAndStream()
{
if (!$this->disk->exists($this->filePath)) {
report(new Exception('S3 File Not Found in S3FileStream - ' . $this->adapterName . ' - ' . $this->disk->path($this->filePath)));
return response('File Not Found', 404);
}

$this->start = 0;
$this->size = $this->disk->size($this->filePath);
$this->end = $this->size - 1;
$this->length = $this->size;
$this->isRange = false;

//Set headers
$this->returnHeaders = [
'Last-Modified' => $this->disk->lastModified($this->filePath),
'Accept-Ranges' => 'bytes',
'Content-Type' => $this->disk->mimeType($this->filePath),
'Content-Disposition' => 'inline; filename=' . ($this->humanName ?? basename($this->filePath) . '.' . Arr::last(explode('.', $this->filePath))),
'Content-Length' => $this->length,
];

//Handle ranges here
if (!is_null(request()->server('HTTP_RANGE'))) {
$cStart = $this->start;
$cEnd = $this->end;

$range = Str::after(request()->server('HTTP_RANGE'), '=');
if (strpos($range, ',') !== false) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}
if (substr($range, 0, 1) == '-') {
$cStart = $this->size - intval(substr($range, 1)) - 1;
} else {
$range = explode('-', $range);
$cStart = intval($range[0]);

$cEnd = (isset($range[1]) && is_numeric($range[1])) ? intval($range[1]) : $cEnd;
}

$cEnd = min($cEnd, $this->size - 1);
if ($cStart > $cEnd || $cStart > $this->size - 1) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}

$this->start = intval($cStart);
$this->end = intval($cEnd);
$this->length = min($this->end - $this->start + 1, $this->size);
$this->returnHeaders['Content-Length'] = $this->length;
$this->returnHeaders['Content-Range'] = 'bytes ' . $this->start . '-' . $this->end . '/' . $this->size;
$this->isRange = true;
}

return $this->stream();
}

/**
* Stream file to client.
* @throws Exception
* @return StreamedResponse
*/
protected function stream(): StreamedResponse
{
$this->adapter->getClient()->registerStreamWrapper();
// Create a stream context to allow seeking
$context = stream_context_create([
's3' => [
'seekable' => true,
],
]);
// Open a stream in read-only mode
if (!($stream = fopen("s3://{$this->adapter->getBucket()}/{$this->filePath}", 'rb', false, $context))) {
throw new Exception('Could not open stream for reading export [' . $this->filePath . ']');
}
if (isset($this->start) && $this->start > 0) {
fseek($stream, $this->start, SEEK_SET);
}

$remainingBytes = $this->length ?? $this->size;
$chunkSize = 100;

$video = response()->stream(
function () use ($stream, $remainingBytes, $chunkSize) {
while (!feof($stream) && $remainingBytes > 0) {
$toGrab = min($chunkSize, $remainingBytes);
echo fread($stream, $toGrab);
$remainingBytes -= $toGrab;
flush();
}
fclose($stream);
},
($this->isRange ? 206 : 200),
$this->returnHeaders
);

return $video;
}
}

Please don't forgot to leave a comment if you like this post. 

Cheers :) 

Happy Coding...

Post a Comment

Previous Post Next Post