拉拉维尔使用S3检索视频以串流

4
我正在通过我的 Laravel 应用将视频存储到 Amazon S3。这很好用。但是我无法“流式传输”它们。
例如,这是 URL:https://website.com/video/342.qt?api_token=a5a18c9f-f5f6-5d66-85e3-aaaaaaa,应该返回名为 '212.DdsqoK1PlL.qt' 的视频文件。
调用该 URL 时返回以下输出: Output of Video URL 这就是视频文件,但我希望它能像这个视频一样直接在浏览器中播放:https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4 该路由调用此函数,从 S3 磁盘检索非公共文件:
public function document(Document $document)
{
    return Storage::disk('s3')->get($document->path);
}

例子中能正常工作的URL和我的唯一区别是,例子是MP4格式,我的是.QT格式,但我尝试了MP4格式,并在浏览器中得到相同的输出;因此没有自动播放视频。

我猜直接播放的电影是在流式传输视频?

我的网站正在Ubuntu上运行,并安装了sudo apt-get install vlc


不要在这里获取文件,使用您的S3文件URL并进行重定向。 - Eduardo Stuart
@EduardoStuart 在哪里获取那个URL?文件未公开列出。 - user1469734
您可以创建一个经过身份验证的URL(临时)或将文件公开。 https://laravel.com/docs/5.5/filesystem#storing-files(可见性部分) 或者 $url = Storage :: temporaryUrl( 'file1.jpg',Carbon :: now()-> addMinutes(5) ); - Eduardo Stuart
2个回答

11

我个人反对将重定向到S3 URL的想法。我通过Laravel php包装器在服务器端遮盖所有的URL。如果其他人遇到类似问题,这就是我用来解决问题的代码。此代码编写用于使用Laravel 5.6从S3流式传输视频(并包括HTTP_RANGE支持,因此它也可以在iOS上工作)。

我使用下面的类,放置在App/Http/Responses中。要使用此类,请创建一个执行此操作的方法(这类似于getFile方法):

$filestream = new \App\Http\Responses\S3FileStream('file_path_and_name_within_bucket', 'disk_bucket_name', 'output_file_name_when_downloaded');
return $filestream->output();

好运的话,您应该很快就可以开始流式传输了(而不会揭示S3 URL)!

S3FileStream.php:<?php

namespace Http\Responses;

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
{
    /**
     * @var \League\Flysystem\AwsS3v3\AwsS3Adapter
     */
    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    = $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;
    }
}

为了我的使用情况,我将“Content-Disposition”头更改为“inline”,而不是附件,以便直接在浏览器中播放视频,而不是下载它。 - rambii
值得注意的是,这对我在使用与S3相同API的Digital Ocean Spaces时也起作用。 - Matthew Daly
我们最初使用这个来解决完全相同的问题,但最近发现它在iOS/Safari上停止工作了。不过,它仍然适用于其他浏览器(例如,当我在那里访问文件时,可以看到Chrome获得206响应)。有人发现了同样的问题吗?目前还不确定这个问题的原因在哪里。 - Joe
1
已更新答案,使用了最新的代码,@Joe!请告诉我它是否有效。 - Jmorko
@Jmorko — 事实证明,问题出在服务器配置上,我们的服务器在HTTP/2连接上返回了一个Connection头,而Safari显然根据HTTP/2标准拒绝了它(https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection)。不过你的更新看起来有一些很好的改进,所以我可能会借鉴你为人名和其他改进所做的工作,谢谢! - Joe
@Jmorko - 你在 Laravel 9 上实现过这个功能吗?看起来好像失败了。我曾经提出过这个问题,但是没有人能够回答。https://dev59.com/OnUOtIcB2Jgan1znuUz0 - Scorekaj22

0

就像我在评论部分所说的那样。我认为你应该使用S3 URL(临时或公共)。

你有一些选择:

  1. 使用Laravel临时URL;
  2. 将文件设置为公共+获取URL;

更多信息请参见: https://laravel.com/docs/5.5/filesystem#storing-files

将文件可见性设置为公共:

Storage::setVisibility('file.jpg', 'public')

临时URL:

$url = Storage::temporaryUrl(
    'file1.jpg', Carbon::now()->addMinutes(5)
);

如果你的文件是公开的,你可以使用:
Storage::url('file1.jpg');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接