megutech

自身の備忘録として主にWEBサーバー周りの技術について投稿しています。

Safariでは206 Partial Contentに対応していないとVideoタグでmp4が再生できない

S3に保存されたmp4動画をPHPを経由して配信していたのだが、iOS/MacSafariだと再生できない事が判明した。

原因はVideoタグなどのリソースはRangeヘッダーを付けてリクエストが投げられるのだが、ここで正しく206 Partial Contentを返してあげないとSafariでは動画を再生できないというものだった。

chromeとかは再生してくれるのにね。流石Safari。すごいぞSafari。滅びればいいのに。

で、以下どうやって対策をとったかという話。

環境

Service Version
PHP 7.2.2
Laravel 6.5.0

問題のコード

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class ResourceController extends Controller
{
    public function download()
    {
        return Storage::disk('s3')->download('your/file/path/movie.mp4', 'movie.mp4');
    }
}

対応

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ResourceController extends Controller
{
    public function download()
    {
        $disk = Storage::disk("s3");
        $size = $disk->size('your/file/path/movie.mp4');

        $start = 0;
        $status = 200;
        $length = $size;

        $headers = [
            'Content-Type' => 'video/mp4',
            'Content-Length' => $size,
            'Accept-Ranges' => 'bytes'
        ];

        if ($range = Request::server('HTTP_RANGE', false)) {
            list($param, $range) = explode('=', $range);
            if (strtolower(trim($param)) !== 'bytes') {
                abort(400);
            }

            list($from, $to) = explode('-', $range);
            if ($from === '') {
                $end = $size - 1;
                $start = $end - intval($from);
            } else if ($to === '') {
                $start = intval($from);
                $end = $size - 1;
            } else {
                $start = intval($from);
                $end = intval($to);
            }
            $length = $end - $start + 1;

            $headers['Content-Length'] = $length;
            $headers['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $size);
            $status = 206;
        }

        $response = new StreamedResponse(function() use ($disk, $start, $length) {
            $stream = $disk->readStream('your/file/path/movie.mp4');
            fseek($stream, $start, SEEK_SET);
            echo fread($stream, $length);
            fclose($stream);
        }, $status, $headers);

        $response->headers->set(
            'Content-Disposition',
            $response->headers->makeDisposition(
                'attachment',
                "movie.mp4",
                str_replace('%', '', Str::ascii("movie.mp4"))
            )
        );

        return $response;
    }
}

上記は実際に書いたコードを何とか一つにまとめたものです。

動作確認はしてないです。(もちろん上記のようにまとめる前の本チャンコードでは動作確認取れてます。)

まあこんな感じ。

めんどくさ。