Mac 和 IOS 14 上的 Safari 无法播放 HTML 5 MP4 视频

Safari on Mac and IOS 14 Won't Play HTML 5 MP4 Video

所以我开发了一个使用 node 的聊天应用程序 back-end。当用户在他们的 iphone 上选择一个视频时,它通常是 .mov 格式,因此当它被发送到节点服务器时,它会用 ffmpeg 转换为 mp4。一切正常,然后如果我在 mac 上的 Chrome 中再次加载我的聊天,视频播放效果与 mp4 一样好。

此屏幕截图显示视频嵌入在那里,设置为 mp4 但它不会在我的 mac 或我的 phone 上的 Safari 中播放,实际上它只是将视频显示为 0 秒很长,但我可以在 chrome 中播放它,还可以通过直接访问嵌入 url 下载 mp4 文件。

有什么想法吗?我将其转换为 mp4 以防止出现此类情况,但 safari 似乎甚至不喜欢 mp4 文件。

提供私人文件的back-end部分在Symfony 4 (PHP):

/**
     * @Route("/private/files/download/{base64Path}", name="downloadFile")
     * @param string $base64Path
     * @param Request $request
     * @return Response
     */
    public function downloadFile(string $base64Path, Request $request) : Response
    {


        // get token
        if(!$token = $request->query->get('token')){
            return new Response('Access Denied',403);
        }



        /** @var UserRepository $userRepo */
        $userRepo = $this->getDoctrine()->getRepository(User::class);

        /** @var User $user */
        if(!$user = $userRepo->findOneBy(['deleted'=>false,'active'=>true,'systemUser'=>false,'apiKey'=>$token])){
            return new Response('Access Denied',403);
        }



        // get path
        if($path = base64_decode($base64Path)){

            // make sure the folder we need exists
            $fullPath = $this->getParameter('private_upload_folder') . '/' . $path;



            if(!file_exists($fullPath)){
                return new Response('File Not Found',404);
            }

        

            $response = new Response();
            $response->headers->set('Content-Type', mime_content_type($fullPath));
            $response->headers->set('Content-Disposition', 'inline; filename="' . basename($fullPath) . '"');
            $response->headers->set('Content-Length', filesize($fullPath));
            $response->headers->set('Pragma', "no-cache");
            $response->headers->set('Expires', "0");
            $response->headers->set('Content-Transfer-Encoding', "binary");

            $response->sendHeaders();

            $response->setContent(readfile($fullPath));

            return $response;
        }

        return new Response('Invalid Path',404);
    }

尝试嵌入视频时,除 Safari 浏览器外,这在任何地方都可以正常工作。这样做是因为视频不是 public 并且需要访问令牌。

更新:这是一个 mp4 测试 link,您必须允许不安全的证书,因为它位于快速测试子域上。如果你在 chrome 中打开它,你会看到我的 3d 打印机固化站的 3 秒视频,如果你在 safari 中加载相同的 link,你会发现它不起作用

https://tester.nibbrstaging.com/private/files/download/Y2hhdC83Nzk1Y2U2MC04MDFmLTExZWItYjkzYy1lZjI4ZGYwMDhkOTMubXA0?token=6ab1720bfe922d44208c25f655d61032

服务器在带有 Apache 的 cPanel 上运行,我认为这可能与需要流式传输的视频有关?

在 SAFARI 中工作但现在在 CHROME 中损坏的更新代码:

Chrome 现在给出 Content-Length: 0 但它在 safari 中运行良好。

public function downloadFile(string $base64Path, Request $request) : ?Response
    {

        ob_clean();

        // get token
        if(!$token = $request->query->get('token')){
            return new Response('Access Denied',403);
        }


        

        /** @var UserRepository $userRepo */
        $userRepo = $this->getDoctrine()->getRepository(User::class);

        /** @var User $user */
        if(!$user = $userRepo->findOneBy(['deleted'=>false,'active'=>true,'systemUser'=>false,'apiKey'=>$token])){
            return new Response('Access Denied',403);
        }



        // get path
        if($path = base64_decode($base64Path)){

            // make sure the folder we need exists
            $fullPath = $this->getParameter('private_upload_folder') . '/' . $path;



            if(!file_exists($fullPath)){
                return new Response('File Not Found',404);
            }


            $filesize = filesize($fullPath);
            $mime = mime_content_type($fullPath);

            header('Content-Type: ' . $mime);

            if(isset($_SERVER['HTTP_RANGE'])){

                // Parse the range header to get the byte offset
                $ranges = array_map(
                    'intval', // Parse the parts into integer
                    explode(
                        '-', // The range separator
                        substr($_SERVER['HTTP_RANGE'], 6) // Skip the `bytes=` part of the header
                    )
                );



                // If the last range param is empty, it means the EOF (End of File)
                if(!$ranges[1]){
                    $ranges[1] = $filesize - 1;
                }

                header('HTTP/1.1 206 Partial Content');
                header('Accept-Ranges: bytes');
                header('Content-Length: ' . ($ranges[1] - $ranges[0])); // The size of the range

                // Send the ranges we offered
                header(
                    sprintf(
                        'Content-Range: bytes %d-%d/%d', // The header format
                        $ranges[0], // The start range
                        $ranges[1], // The end range
                        $filesize // Total size of the file
                    )
                );

                // It's time to output the file
                $f = fopen($fullPath, 'rb'); // Open the file in binary mode
                $chunkSize = 8192; // The size of each chunk to output

                // Seek to the requested start range
                fseek($f, $ranges[0]);

                // Start outputting the data
                while(true){
                    // Check if we have outputted all the data requested
                    if(ftell($f) >= $ranges[1]){
                        break;
                    }

                    // Output the data
                    echo fread($f, $chunkSize);

                    // Flush the buffer immediately
                    @ob_flush();
                    flush();
                }
            }else{

                // It's not a range request, output the file anyway
                header('Content-Length: ' . $filesize);

                // Read the file
                @readfile($filesize);

                // and flush the buffer
                @ob_flush();
                flush();



            }

        }else {

            return new Response('Invalid Path', 404);
        }
    }

我在 chrome 中注意到它发送的范围 header 是这样的:

范围:字节=611609-

Safari 传送到哪里

范围:字节=611609-61160

所以出于某种原因 chrome 缺少第二个范围数量,这显然意味着我的代码无法找到第二个范围编号。

无论我做什么,我都无法让它在 chrome 和 safari 中正常工作。 Safari 想要字节范围部分,chrome 似乎请求它然后发送对完整文件的新请求,但即使是代码的完整文件部分也会出现 500 错误。如果我取出字节范围位,那么它在 chrome 中工作正常,但在 safari 中工作不正常。

更新:

chrome 中发生了一些奇怪的事情:

对于我正在测试的视频,它提出了 3 个范围请求:

请求 1 HEADERS - 请求字节 0-(到文件末尾)

GET /private/files/download/Y2hhdC83Nzk1Y2U2MC04MDFmLTExZWItYjkzYy1lZjI4ZGYwMDhkOTMubXA0?token=6ab1720bfe922d44208c25f655d61032 HTTP/1.1

Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36
Accept-Encoding: identity;q=1, *;q=0
Accept: */*
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: video
Referer: https://gofollow.vip/
Accept-Language: en-US,en;q=0.9
Range: bytes=0-

响应将文件中的所有字节返回给 CHROME:

HTTP/1.1 206 Partial Content
Date: Wed, 10 Mar 2021 12:35:54 GMT
Server: Apache
Accept-Ranges: bytes
Content-Length: 611609
Content-Range: bytes 0-611609/611610
Vary: User-Agent
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: video/mp4

第二个请求HEADERS:现在它在文件末尾请求 589824:

Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36
Accept-Encoding: identity;q=1, *;q=0
Accept: */*
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: video
Referer: https://gofollow.vip/
Accept-Language: en-US,en;q=0.9
Range: bytes=589824-

响应义务:

HTTP/1.1 206 Partial Content
Date: Wed, 10 Mar 2021 12:35:55 GMT
Server: Apache
Accept-Ranges: bytes
Content-Length: 21785
Content-Range: bytes 589824-611609/611610
Vary: User-Agent
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: video/mp4

然后它发出第三个请求导致内部服务器错误,这次它实际上是在请求最后一个字节:

GET /private/files/download/Y2hhdC83Nzk1Y2U2MC04MDFmLTExZWItYjkzYy1lZjI4ZGYwMDhkOTMubXA0?token=6ab1720bfe922d44208c25f655d61032 HTTP/1.1

Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36
Accept-Encoding: identity;q=1, *;q=0
Accept: */*
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: video
Referer: https://gofollow.vip/
Accept-Language: en-US,en;q=0.9
Range: bytes=611609-

响应 - 内容长度为 0,因为请求的字节和返回的字节之间没有差异:

HTTP/1.1 500 Internal Server Error
Date: Wed, 10 Mar 2021 12:35:56 GMT
Server: Apache
Accept-Ranges: bytes
Cache-Control: max-age=0, must-revalidate, private
X-Frame-Options: DENY
X-XSS-Protection: 1
X-Content-Type-Options: nosniff
Referrer-Policy: origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Expires: Wed, 10 Mar 2021 12:35:56 GMT
Content-Length: 0
Content-Range: bytes 611609-611609/611610
Vary: User-Agent
Connection: close
Content-Type: text/html; charset=UTF-8

毕竟我终于找到了问题所在。它要求第三个请求中的最后一个字节和以下代码来计算发回内容的大小:

header('Content-Length: ' . ($ranges[1] - $ranges[0])); // The size of the range

生成的长度为 0 作为最后一个字节,直到文件末尾为 0。所以我将其更改为加 1 到末尾:

header('Content-Length: ' . (($ranges[1] - $ranges[0])+1)); // The size of the range

我以为我可能对此有疑问,但事实证明它适用于两种浏览器