Я уверен, что много разработчиков сталкивались с проблемой когда надо максимально быстро воспроизводить огромные видео файлы (4 GB+) на Вашем сайте. Я тоже люблю посмотреть фильмы в онлайн но мне так надоедает ждать, когда он про грузится перед просмотром.
Столкнулся с интересной проблемой а именно с video stream в браузере Internet Explorer 11 который все так обожают. Теперь перейдем к деталям.
Продукт написан на Laravel 5.3, а для транскодирувания видео файла мы используем ffmpeg (создание preview, thumbnail нужных нам размеров, качества + watermark).
Первое на что надо обратить внимание — это атомы. Видео состоит с атомов, в которых храниться информация об субтитрах, главах, видео и аудио и тд… Но особое внимание надо обратить на moov атом. В нем храниться информация как воспроизвести видео, сколько кадров в секунду, какие размеры. Этот атом может лежать где угодно. Если видео маленькое, оно быстро загрузится, в результате уже есть moov атом и система знает как воспроизвести видео.
Но а как же быть если у нас огромные видео файлы? Нужно настроить video stream (потоковую передачу видео файла).
Простыми словами работа происходит так, система делает первый запрос и получает начало видео файла и пытается там найти moov атом, если его нет — делает второй запрос с конца. В конце концов moov атом найден и происходит третий запрос на получение нужного кусочка видео файла для того что бы воспроизвести видео.
Поиск moov атома обычно занимает не очень много времени но почему бы не решить эту проблему с помощью FFmpeg.
Как я ранее писал, Мы используем FFmpeg для транскодирования видео файлов. Что бы поменять порядок атомов и передвинуть moov атом в самое начало можно воспользоватся -movflags faststart:
ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4
Таким образом можно решить данную проблему, вместо трех запросов:
будет только два:
Что бы работала потоковая передача файла нужно передавать нужные заголовки, а именно Accept-Ranges: bytes и сам диапазон 50-100/5644, где:
50 — с чего продолжим загрузку;
100 — когда закончим;
5644 — вес файла.
Что бы проверить настроен ли Ваш сервер правильно, выполните команду в терминале:
$ curl -i -X HEAD --header "Range: bytes=50-100" http://вашСайт/вашРесурс.mp4
пример правильного ответа
HTTP/1.1 206 Partial Content
Date: Wed, 08 Nov 2017 13:58:06 GMT
Server: Apache
Last-Modified: Thu, 03 Nov 2016 15:14:44 GMT
ETag: "a002df-160c-54067aa74702f"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Range: bytes 50-100/5644
Content-Type: video/mp4
В Laravel есть возможность использоваться disk driver что бы получать, загружать ресурсы на свой сервер или на S3 облако и другие облака.
Например:
Storage::disk('local')->get('video.mp4');
или
Storage::disk('s3')->get('video.mp4');
Моя проблема заключалась в том что мне надо производить потоковую передачу видео файла используя Storage::disk('local').
Пошел самым простом способом, воспользовался PHP VideoStream class for HTML5 video streaming который написал то ли Vagner Luz do Carmo то ли Rana Md Ali Ahsan, спасибо им. Но внес некоторые изменения для того что бы передавать конкретный тип ресурса и что бы video stream работал в IE11.
Реализация:
В нужном мне методе проверяю наличие файла и потом передаю путь к файлу и тип файла:
if (Storage::disk(config('be_storage.default_disk'))->exists($path)) {
$videoPath=$filesystem->getAdapter()->getPathPrefix().$path;
$stream = new VideoStream($videoPath,$type);
return response()->stream(function() use ($stream) {
$stream->start();
});
}
return response("File doesn't exists", 404);
Теперь мною усовершенствованный VideoStream.class
<?php
namespace BEStorageServer;
/**
* Description of VideoStream
*
* @author Rana
* @link http://codesamplez.com/programming/php-html5-video-streaming-tutorial
*/
class VideoStream
{
private $path = "";
private $type = "";
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
function __construct($filePath, $type)
{
$this->path = $filePath;
$this->type = $type;
}
/**
* 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: " . $this->type);
// 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);
header("Accept-Ranges: bytes");
}
}
/**
* 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();
}
}
Самое важное изменения, которое позволило работать video stream в IE11 - это добавление header("Accept-Ranges: bytes"); без диапазона при первом Response когда не существует $_SERVER['HTTP_RANGE'].
Что и как должно работать
Первый Response должен иметь такие заголовки:
Accept-Ranges:bytes
Connection:Keep-Alive
Content-Length:1342588187
Content-Type:video/mp4
В втором запросе в Request Headers Вы увидите Range:bytes=0- , это означает что Мы требуем начало файла и отравляем Response:
Accept-Ranges:0-2127552
Connection:Keep-Alive
Content-Length:1342588187
Content-Range:bytes 0-1342588186/1342588187
Content-Type:video/mp4
Если Мы переключаем на средину видео - на сервер отправляется Request :
Range:bytes=325550080-1342588186
а Response должен быть таким:
Accept-Ranges:0-1342588186
Connection:Keep-Alive
Content-Length:1017038107
Content-Range:bytes 325550080-1342588186/1342588187
и моментально происходит воспроизведение видео с нужной нам точки.
Спасибо за внимание!
Автор: Александр Бабич
отличное решение