У меня есть ситуация, в которой я поставлен в тупик, поэтому я ищу любую помощь, которую я могу получить.
У меня есть приложение для iOS, которое использует MPMoviePlayerViewController для воспроизведения видеофайлов M4V, управляемых сайтом Laravel 5.
Видеофайлы отлично воспроизводятся (в iOS), если они загружаются напрямую из папки Laravel 5 / public. Тем не менее, я обычно храню и обслуживаю видеофайлы с хранилища Laravel 5, поскольку в конечном итоге собираюсь использовать S3 и эластичный транскодер.
Это работает в FireFox с плагином браузера QuickTime, VLC и другими клиентами потокового видео, но не с нашим приложением для iOS.
Насколько я могу судить, MPMoviePlayerViewController требователен к форматированию ответа HTTP. Я попробовал StreamedResponse, но это, похоже, не помогает.
Так, например, следующий URL, который извлекает файл непосредственно из папки / public, отлично работает в iOS:
http://172.16.160.1/video_ae9a7da0efa211e4b115f73708c37d67.m4v
Но если я использую Laravel 5, чтобы извлечь файл из хранилища с этим URL, iOS не будет его воспроизводить.
http://172.16.160.1/api/getfile/f444b190ef5411e4b7068d1890d109e8/video_ae9a7da0efa211e4b115f73708c37d67.m4v
Обратите внимание, что iOS не предоставляет каких-либо значимых ошибок, чтобы помочь отладить это, но я уверен, что мой ответ HTTP сделан Laravel 5.
Вот мой маршрут:
Route::get('myapi/getfile/{filename?}', 'APIController@getfile')->where('filename', '(.*)');
Вот мой контроллер:
public function getfile($filename)
{
return $api = API::getfile($filename);
}
Вот моя модель:
public static function getfile($filename) {
$file = Storage::disk('local')->get('Files/'.$filename);
return (new Response($file, 200))->header('Content-Type', 'video/mp4');
}
Если я опущу какую-либо вспомогательную информацию, пожалуйста, дайте мне знать, и я опубликую ее. Моим следующим шагом может быть установка тестового стенда Wireshark и просмотр того, как выглядит рукопожатие.
Заранее спасибо за помощь. 🙂
Похоже, у меня есть ответ на свой вопрос. Основная причина заключалась в том, что Laravel 5 не поддерживает запросы HTTP в диапазоне байтов при обслуживании файлов.
Этот пост, расположенный здесь, заставил меня встать на правильный путь:
MPMoviePlayerPlaybackDidFinishNotification вызывается немедленно
Затем я нашел два сообщения об этом Laravel 5:
http://laravel.io/forum/09-23-2014-how-to-support-http-byte-serving-in-file-streams
https://gist.github.com/m4tthumphrey/b0369c7bd5e2c795f6d5
Единственный недостаток — я не могу использовать Storage Facade для прямого доступа к файлам в виде потоков. Таким образом, это решение может использоваться только для файлов, расположенных в локальной файловой системе.
public static function getfile($filename) {
$size = Storage::disk('local')->size('files/'.$filename);
$file = Storage::disk('local')->get('files/'.$filename);
$stream = fopen($storage_home_dir.'files/'.$filename, "r");
$type = 'video/mp4';
$start = 0;
$length = $size;
$status = 200;
$headers = ['Content-Type' => $type, 'Content-Length' => $size, 'Accept-Ranges' => 'bytes'];
if (false !== $range = Request::server('HTTP_RANGE', false)) {
list($param, $range) = explode('=', $range);
if (strtolower(trim($param)) !== 'bytes') {
header('HTTP/1.1 400 Invalid Request');
exit;
}
list($from, $to) = explode('-', $range);
if ($from === '') {
$end = $size - 1;
$start = $end - intval($from);
} elseif ($to === '') {
$start = intval($from);
$end = $size - 1;
} else {
$start = intval($from);
$end = intval($to);
}
$length = $end - $start + 1;
$status = 206;
$headers['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $size);
}
return Response::stream(function() use ($stream, $start, $length) {
fseek($stream, $start, SEEK_SET);
echo fread($stream, $length);
fclose($stream);
}, $status, $headers);
}
Я знаю, что это старый пост, но в итоге мне пришлось транслировать видео в Laravel с S3 на плеер, который требовал поддержки HTTP_RANGE. Я положил это вместе (после прочтения многих тем). Это должен Поддержите все диски (), которые вы определили в Laravel.
Я использовал класс ниже, размещенный в App / Http / Responses. Чтобы использовать этот класс, создайте метод, который делает это (это будет содержимым вашего метода getFile):
$filestream = new \App\Http\Responses\S3FileStream('file_path_and_name_within_bucket', 'disk_bucket_name');
return $filestream->output();
Я только что указал src моего видеоплеера на путь для этого метода и успеха!
S3FileStream.php:
namespace App\Http\Responses;
use Illuminate\Http\Request;
use Storage;
class S3FileStream
{
/**
* @var \League\Flysystem\AwsS3v3\AwsS3Adapter
*/
private $adapter;
/**
* @var \Aws\S3\S3Client
*/
private $client;
/**
* @var file end byte
*/
private $end;
/**
* @var string
*/
private $filePath;
/**
* @var bool storing if request is a range (or a full file)
*/
private $isRange = false;
/**
* @var length of bytes requested
*/
private $length;
/**
* @var
*/
private $return_headers = [];
/**
* @var file size
*/
private $size;
/**
* @var start byte
*/
private $start;
/**
* S3FileStream constructor.
* @param string $filePath
* @param string $adapter
*/
public function __construct(string $filePath, string $adapter = 's3')
{
$this->filePath = $filePath;
$this->filesystem = Storage::disk($adapter)->getDriver();
$this->adapter = Storage::disk($adapter)->getAdapter();
$this->client = $this->adapter->getClient();
}
/**
* Output file to client
*/
public function output()
{
return $this->setHeaders()->stream();
}
/**
* Output headers to client
* @return $this
*/
protected function setHeaders()
{
$object = $this->client->headObject([
'Bucket' => $this->adapter->getBucket(),
'Key' => $this->filePath,
]);
$this->start = 0;
$this->size = $object['ContentLength'];
$this->end = $this->size - 1;
//Set headers
$this->return_headers = [];
$this->return_headers['Last-Modified'] = $object['LastModified'];
$this->return_headers['Accept-Ranges'] = 'bytes';
$this->return_headers['Content-Type'] = $object['ContentType'];
$this->return_headers['Content-Disposition'] = 'attachment; filename=' . basename($this->filePath);
if (!is_null(request()->server('HTTP_RANGE'))) {
$c_start = $this->start;
$c_end = $this->end;
[$_, $range] = explode('=', request()->server('HTTP_RANGE'), 2);
if (strpos($range, ',') !== false) {
headers('Content-Range: bytes ' . $this->start . '-' . $this->end . '/' . $this->size);
return response('416 Requested Range Not Satisfiable', 416);
}
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) {
headers('Content-Range: bytes ' . $this->start . '-' . $this->end . '/' . $this->size);
return response('416 Requested Range Not Satisfiable', 416);
}
$this->start = $c_start;
$this->end = $c_end;
$this->length = $this->end - $this->start + 1;
$this->return_headers['Content-Length'] = $this->length;
$this->return_headers['Content-Range'] = 'bytes ' . $this->start . '-' . $this->end . '/' . $this->size;
$this->isRange = true;
} else {
$this->length = $this->size;
$this->return_headers['Content-Length'] = $this->length;
unset($this->return_headers['Content-Range']);
$this->isRange = false;
}
return $this;
}
/**
* Stream file to client
* @throws \Exception
*/
protected function stream()
{
$this->client->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)) {
fseek($stream, $this->start, SEEK_SET);
}
$remaining_bytes = $this->length ?? $this->size;
$chunk_size = 1024;
$video = response()->stream(
function () use ($stream, $remaining_bytes, $chunk_size) {
while (!feof($stream) && $remaining_bytes > 0) {
echo fread($stream, $chunk_size);
$remaining_bytes -= $chunk_size;
flush();
}
fclose($stream);
},
($this->isRange ? 206 : 200),
$this->return_headers
);
return $video;
}
}