IOS safari 播放 mp4 遇到的坎儿

起因

事情的起因是调试 IOS 手机下播放服务器接口返回的 mp4 文件流失败。对于没调试过移动端和 Safari 的我来说着实费了些功夫,网上和AI也没有讲明白。好在最终大概理清楚了,在这里整理出来供有缘人参考。

问题

因为直接用 IOS 手机的浏览器打开页面去播放也能复现,所以问题出现在 IOS上的 Safari 浏览器上。

问题主要分两类:

  • 一、视频能播放,但是不主动设置 poster,就不显示默认的 poster
  • 二、视频不能播放

首先文件是没有问题的,是主流浏览器都支持的视频编码格式为 H.264(AVC).mp4 文件,由文件格式导致的播放失败问题,网上很多,这里就不赘述了。

问题复现和解决

因为其他浏览器可以正常播放,所以用来对比差异时,我直接在PC端(Edge)查看信息,而 Safari 是在手机上访问项目页面,以及局域网下访问本地运行的 web 服务页面。

用到的工具如下:

  • Live Server 用来运行本地 web 页面
  • node + nodemon + Express 用来运行文件服务,模拟后端接口,nodemon 方便实时同步运行修改内容
  • video.js 播放视频的插件,直接用 CDN。
  • vConsole 移动端查看控制台,直接用 CDN

编写简单 demo

排查问题的时候我是一点一点将实现方式还原到最原始的方式:

  1. 后端接口 + video.js
  2. 后端接口 + video标签
  3. 本地运行的接口 + video标签
  4. 本地文件 + video标签

所以这里再梳理,就可以反向从最简单的方式排查,在本地写个用本地视频文件 + video标签的demo,为了后续排查方便,将事件日志也打印出来。

视频资源我用的 https://vjs.zencdn.net/v/oceans.mp4 下载到本地,不过它的开头是黑屏淡入的,为了方便区分 “不显示poster”,我将开头的几秒黑屏裁剪掉了。
也可以用安卓手机录制一个,默认就是支持播放的 mp4 文件。(PS:别用QQ录屏,编码不对)

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script><script>var vConsole = new window.VConsole()</script><style>#video {width: 480px;height: 25vh;max-width: 100%;/* 背景色用于在视频不显示poster时查看占位 */background: pink;border: 4px solid pink;}</style></head><body><div class="video-box"><video id="video" src="oceans.mp4" controls preload="auto" playsinline="true" muted></video></div><div id="log"><h2>日志:方便手机上的 Safari 查看</h2><div class="log__content"></div></div><script>// 打印日志function log(msg) {// 页面上打印日志const logContentEl = document.querySelector('.log__content')const pEl = document.createElement('p')pEl.textContent = msglogContentEl.appendChild(pEl)// 控制台打印日志console.log(msg)}const video = document.getElementById('video')// 主要关心的事件const eventNames = [{ name: 'abort', desc: '当音频/视频的加载已放弃时触发' },{ name: 'canplay', desc: '当浏览器可以开始播放音频/视频时触发' },{ name: 'canplaythrough', desc: '当浏览器预计能够在不因缓冲而停顿的情况下持续播放指定的音频/视频时触发' },{ name: 'durationchange', desc: '当音频/视频的时长已更改时触发' },{ name: 'error', desc: '当在音频/视频加载期间发生错误时触发' },{ name: 'loadeddata', desc: '当浏览器已加载音频/视频的当前帧时触发' },{ name: 'loadedmetadata', desc: '当浏览器已加载音频/视频的元数据时触发' },{ name: 'loadstart', desc: '当浏览器开始查找音频/视频时触发' },{ name: 'play', desc: '当音频/视频已开始或不再暂停时触发' },{ name: 'playing', desc: '当音频/视频在因缓冲而暂停或停止后已就绪时触发' },{ name: 'progress', desc: '当浏览器正在下载音频/视频时触发' },{ name: 'timeupdate', desc: '当音频/视频的播放位置发生改变时触发' },{ name: 'waiting', desc: '当视频由于需要缓冲下一帧而停止,等待时触发' }]// 注册video事件监听器eventNames.forEach(v => {video.addEventListener(v.name, () => {// 打印日志log(`【readyState: ${video.readyState}${v.name}: ${v.desc}`)})})</script></body>
</html>

preload 默认是 metadata,和 auto 的日志结果一样。为了排除一些可能性,我将其设置了 auto
playsinline="true" 防止 IOS 播放视频时自动打开全屏。

问题1 不显示预览图

demo 页面加载后发现,Safari 浏览器没有显示视频的预览图:

在这里插入图片描述
Edge 显示了预览图:

在这里插入图片描述
通过日志发现,Safari 加载完元数据后(loadedmetadata)就不会继续加载了。

我们知道,视频的预览图就是 video 标签的 poster 属性,当 poster 有值时就会显示指定的图片,当 poster 没有值时,浏览器就会自动处理,而不同浏览器的处理方式也不一样,可能会有这几种情况:

  1. 如果配置了预加载,显示加载后的第一帧,或第二、三桢。
  2. 什么都不显示,或显示默认的播放器背景样式

从日志可以看到,Safari 只加载了元数据,并没有加载画面。查看官方文档,得到了解答:

在这里插入图片描述
再看 poster 的说明:
在这里插入图片描述
既然如此,那只能提供一个 poster 来解决了。

问题2 视频不能播放

问题复现

播放 demo 示例里的视频,是可以正常播放的。

将播放方式改成 video.js + 本地文件,播放是正常的。代码如下:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><link href="https://unpkg.com/video.js@7.10.2/dist/video-js.min.css" rel="stylesheet" /><script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script><script>var vConsole = new window.VConsole()</script><style>#video {width: 480px;max-width: 100%;height: 25vh;/* 背景色用于在视频不显示poster时查看占位 */background: pink;border: 4px solid pink;}</style></head><body><video id="video" class="video-js" controls preload="auto" playsinline="true" muted><source src="oceans.mp4" type="video/mp4" /></video><div id="log"><h3>日志:方便手机上的 Safari 查看</h3><div class="log__content"></div></div><script src="https://unpkg.com/video.js@7.10.2/dist/video.min.js"></script><script>// 打印日志function log(msg) {// 页面上打印日志const logContentEl = document.querySelector('.log__content')const pEl = document.createElement('p')pEl.textContent = msglogContentEl.appendChild(pEl)// 控制台打印日志console.log(msg)}// 主要关心的 videojs 事件const eventNames = [{ name: 'abort', desc: '当音频/视频的加载已放弃时触发' },{ name: 'canplay', desc: '当浏览器可以开始播放音频/视频时触发' },{ name: 'canplaythrough', desc: '当浏览器预计能够在不因缓冲而停顿的情况下持续播放指定的音频/视频时触发' },{ name: 'durationchange', desc: '当音频/视频的时长已更改时触发' },{ name: 'error', desc: '当在音频/视频加载期间发生错误时触发' },{ name: 'loadeddata', desc: '当浏览器已加载音频/视频的当前帧时触发' },{ name: 'loadedmetadata', desc: '当浏览器已加载音频/视频的元数据时触发' },{ name: 'loadstart', desc: '当浏览器开始查找音频/视频时触发' },{ name: 'play', desc: '当音频/视频已开始或不再暂停时触发' },{ name: 'playing', desc: '当音频/视频在因缓冲而暂停或停止后已就绪时触发' },{ name: 'progress', desc: '当浏览器正在下载音频/视频时触发' },{ name: 'timeupdate', desc: '当音频/视频的播放位置发生改变时触发' },{ name: 'waiting', desc: '当视频由于需要缓冲下一帧而停止,等待时触发' }]const player = videojs('video')player.ready(() => {// 注册video事件监听器eventNames.forEach(v => {player.on(v.name, () => {// 打印日志log(`【readyState: ${player.readyState()}${v.name}: ${v.desc}`)})})})</script></body>
</html>

然后换成后端接口地址,Edge 可以播放,但是 Safari 就失败了。

video 标签方式:
在这里插入图片描述
video.js 方式:报错 The media could not be loaded, either because the server or network failed or because the format is not supported.
在这里插入图片描述

我肯定不是服务器网络故障,也不是文件格式不支持,所以原因只能是接口了。

本地模拟接口

为了不麻烦后端同事,我只能在本地搭建一个服务器,模拟项目接口。

搭建服务

创建 app.js 文件

const express = require('express')
const fs = require('fs')
const path = require('path')const app = express()app.get('/', (req, res) => {res.send('Stupid IOS')
})app.listen(3000, () => {const ip = 192.169.3.7 // 我的局域网ipconsole.log(`server is running on http://${ip}:3000`)
})
# 安装依赖
npm i express
# 已安装 nodemon,所以直接使用
nodemon app.js

访问 http://192.169.3.7:3000/。把 oceans.mp4 视频文件放到 app.js 同目录下,后面就编写接口就行。

方式1 使用 express 封装好的方法返回文件流
// 方式1: 使用封装好的方法返回文件流
app.get('/type1/:file', (req, res) => {const fileName = req.params.fileconst filePath = path.join(__dirname, fileName)res.sendFile(filePath)
})

文件地址:http://192.169.3.7:3000/type1/oceans.mp4

video标签和 video.js 播放都正常。

方式2 手动读取全部文件流并返回

同样是接口,本地模拟的正常,项目接口不能播放,那就继续更细致的模拟,手动读取文件流,设置相同的响应头。

查看项目接口的响应头,排除一些范围,只模拟有可能影响的:

// 方式2: 手动读取全部文件流并返回
app.get('/type2/:file', (req, res) => {const fileName = req.params.fileconst filePath = path.join(__dirname, fileName)// 打印请求头console.log(req.headers)fs.stat(filePath, (err, stats) => {if (err) {return res.status(404).send('file not found')}// 设置响应头res.set({'Accept-Ranges': 'bytes', // 支持 Range 请求'Cache-Control': 'no-cache, no-store', // 不缓存'Content-Type': 'video/mp4;charset=UTF-8','Content-Range': `bytes 0-${stats.size - 1}/${stats.size}`,'Content-Length': stats.size,'Content-Disposition': 'attachment;filename="oceans.mp4"',ETag: `"${stats.ino.toString()}-${stats.size.toString()}-${Date.now().toString()}"`,'Last-Modified': stats.mtime.toUTCString(),'Pragma': 'no-cache'})const stream = fs.createReadStream(filePath)stream.pipe(res)})
})

文件地址:http://192.169.3.7:3000/type2/oceans.mp4

问题依旧存在,于是查看官方文档,找到一段说明:
在这里插入图片描述

Range 请求就是范围请求或分块传输,客户端通过请求头 Range 指定当前请求想要获取的数据子节范围,服务器根据这个范围,读取文件流,并将读取的内容返回给客户端。
通常,服务器会返回 206 状态码,表示范围请求的响应结果。并且需要在响应头中包含 Content-Range 字段,指明实际返回的数据范围,以及整个资源的总大小。

可是我加了支持 range 请求的响应头啊:'Accept-Ranges': 'bytes'

继续看文档,下面介绍了如何确认服务器是否支持 range 请求:
在这里插入图片描述
大概意思就是主动发送一个指定范围 100 bytes 的请求,看返回的数据是100 bytes,那就是支持,如果返回了整个文件,那就是不支持。

查看之前服务器中打印的请求头,Range 请求头的值:

  • Edge 是 0-,表示获取整个资源
  • Safari 是 0-1:表示获取位置01 的子节的资源,注意可不是从开头到第1个子节,这个范围的请求数是2个子节。

因为我每次都返回的完整的文件流,没有按照 Safari 的要求范围处理,所以属于不支持。

看来仅仅配置响应头是不行的,还要正确处理请求头中的指定范围。

方式3 手动读取指定范围的文件流并返回-分块传输

原来 Safari 会先发送一个获取范围为 bytes=0-1 的请求,以测试服务器是否支持 range 请求。

于是我手动改了下响应头,去掉那些没有影响的,还是返回整个文件流:

    // 设置响应头res.set({'Accept-Ranges': 'bytes', // 支持 Range 请求'Cache-Control': 'no-cache, no-store', // 不缓存'Content-Type': 'video/mp4;charset=UTF-8','Content-Range': `bytes 0-1/${stats.size}`,'Content-Length': stats.size,})

看来仅仅伪造响应头还是不行,还要返回正确大小的文件流,那就编写分块传输的接口:

// 方式3: 手动读取指定范围的文件流并返回-分块传输
app.get('/type3/:file', (req, res) => {const fileName = req.params.fileconst filePath = path.join(__dirname, fileName)// 查看请求头的范围console.log(req.headers.range)fs.stat(filePath, (err, stats) => {if (err) {return res.status(404).send('file not found')}// 设置响应头res.set({'Accept-Ranges': 'bytes', // 支持 Range 请求'Cache-Control': 'no-cache, no-store', // 不缓存'Content-Type': 'video/mp4'})const range = req.headers.rangelet partslet start = 0let end = stats.size - 1if (range) {parts = range.replace(/bytes=/, '').split('-')start = parseInt(parts[0], 10)end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1}if (start >= stats.size || end > stats.size) {// 如果Range请求超出文件大小,返回416状态码return res.status(416).end()}if (start >= 0 && end >= 0) {// 处理 Range 请求res.set({'Content-Range': `bytes ${start}-${end}/${stats.size}`,'Content-Length': end - start + 1})// 部分内容状态码,返回200浏览器也能正常处理res.status(206)}const stream = fs.createReadStream(filePath, { start, end })stream.pipe(res)})
})

终于视频可以正常播放了,video.js 也可以播放。

Safari 的校验还挺严格,如果服务器正确处理了这个2子节的请求,Safari 就会开始正式发送正常范围的请求。

服务器的日志可以体现出来:

bytes=0-1
bytes=0-51883958
bytes=196608-51883958
bytes=458752-51883958

最终查看了后端代码,果然接口没有处理 range 请求,在修改逻辑后,功能终于正常。

继续伪造 range 接口

为了搞清楚 Safari 的校验到底有多严格,我再次尝试模拟了一下第一次请求的响应:

	const range = req.headers.rangelet partslet start = 0let end = stats.size - 1// 增加逻辑 start-----------------------------------------if (range === 'bytes=0-1') {// 设置响应头res.set({'Accept-Ranges': 'bytes', // 支持 Range 请求'Cache-Control': 'no-cache, no-store', // 不缓存'Content-Type': 'video/mp4;charset=UTF-8','Content-Range': `bytes 3-3/${stats.size}`, // 随便写个范围'Content-Length': 2})const stream = fs.createReadStream(filePath, { start:100, end:200 }) // 随便获取个范围,但不能少于2stream.pipe(res)return res.status(206) // 随便返回个状态码}// 增加逻辑 end-----------------------------------------if (range) {parts = range.replace(/bytes=/, '').split('-')start = parseInt(parts[0], 10)end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1}

再次提醒,bytes=0-1 表示请求的位置是01的子节,是2个子节,而不是 1-0=1 的子节数量。

不断测试下,发现只要满足这几个要求,Safari 就认为接口支持 range 请求:

  1. Content-Length 要正确。
  2. Content-Range 的范围要合理。
  3. 返回了不少于 Range 请求头要求大小的文件流数据。

而下面这几点,不影响 Safari 的校验结果:

  1. Content-Range 的范围和 Range 的指定范围不一样
  2. 读取的数据范围和 Range 的指定范围不一样
  3. 返回任意状态码

总结

  1. IOS 上的 Safari 不支持 video 预加载(preload),浏览器不会自动提取帧画面作为默认的 poster 预览图
  2. Safari 上使用 video 播放视频,必须支持并正确处理 Range 范围请求,浏览器会先发送 bytes=0-1 范围的请求来测试服务器是否支持 Range 请求,如果校验成功,就会继续发送正常范围的 Range 请求。否则不再请求资源。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/498331.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kubernetes Gateway API-2-跨命名空间路由

1 跨命名空间路由 Gateway API 具有跨命名空间路由的核心支持。当多个用户或团队共享底层网络基础设施时,这很有用,但必须对控制和配置进行分段,以尽量减少访问和容错域。 Gateway 和 Route(HTTPRoute,TCPRoute,GRPCRoute) 可以部署到不同的命名空间中,路由可以跨命名空间…

第十六届蓝桥杯模拟赛(第一期)(C语言)

判断质因数 如果一个数p是个质数&#xff0c;同时又是整数a的约数&#xff0c;则p称为a的一个质因数。 请问2024有多少个质因数。 了解 约数&#xff0c;又称因数。整数a整除整数b&#xff0c;b为a的因数&#xff08;约数&#xff09;质数&#xff0c;又称素数。只有1和它本身两…

AI安全的挑战:如何让人工智能变得更加可信

引言 随着人工智能&#xff08;AI&#xff09;技术在各个领域的广泛应用&#xff0c;尤其是在医疗、金融、自动驾驶和智能制造等行业&#xff0c;AI正在重塑我们的工作和生活方式。从提高生产效率到实现个性化服务&#xff0c;AI带来了前所未有的便利。然而&#xff0c;在享受这…

TiDB 的MPP架构概述

MPP架构介绍&#xff1a; 如图&#xff0c;TiDB Server 作为协调者&#xff0c;首先 TiDB Server 会把每个TiFlash 拥有的region 会在TiFlash上做交换&#xff0c;让表连接在一个TiFlash上。另外 TiFlash会作为计算节点&#xff0c;每个TiFlash都负责数据交换&#xff0c;表连接…

springboot499基于javaweb的城乡居民基本医疗信息管理系统(论文+源码)_kaic

摘 要 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff0c;还是可操作性等各个方面来讲&#xff0c;遇到了互联网时代才发现能补上自古…

【SQL Server】教材数据库(1)

1 利用sql建立教材数据库&#xff0c;并定义以下基本表&#xff1a; 学生&#xff08;学号&#xff0c;年龄&#xff0c;性别&#xff0c;系名&#xff09; 教材&#xff08;编号&#xff0c;书名&#xff0c;出版社编号&#xff0c;价格&#xff09; 订购&#xff08;学号…

RT-Thread中堆和栈怎么跟单片机内存相联系

现在RT-ThreadMCU的应用方式越来越普遍&#xff0c;RT-Thread需要配置MCU中的RAM到的系统中&#xff0c;进入系统内存管理&#xff0c;才能提供给基于实时系统的应用程序使用&#xff0c;比如给应用程序提供malloc、free等函数调用功能。在嵌入式软件开发中&#xff0c;我们经常…

Linux硬盘分区 --- fdisk命令MBR分区、添加硬盘、lsblk命令

一、MBR分区 如果想对硬盘进行分区可以使用“ fdisk ”命令&#xff0c;它会采用MBR格式将硬盘进行分区。MBR是传统的分区机制&#xff0c;支持 32 位和 64 位系统&#xff0c;最多只能创建 4 个主分区&#xff0c;或者 3 个主分区和 1 个扩展分区&#xff0c;只支持不超过 2T…

GraphRAG 框架哪家强?选择最适合你智能问答系统的框架

GraphRAG 框架哪家强&#xff1f;选择最适合你智能问答系统的框架 点击进入&#xff1a;GraphRAG系列文章-Nano-GraphRAG&#xff1a;打造轻量级医疗诊断助手 点击进入&#xff1a;GraphRAG系列文章-突破传统知识管理瓶颈&#xff1a;LlamaIndex GraphRAG 让企业知识问答更智能…

day-102 二进制矩阵中的最短路径

思路 BFS 解题过程 从起点依次向八个方向尝试&#xff08;之后也一样&#xff09;&#xff0c;如果某个位置在矩阵内且值为0且没有访问过&#xff0c;将其添加到一个队列中&#xff0c;依次类推&#xff0c;直到到达出口 Code class Solution {public int shortestPathBinar…

vue3学习笔记(10)-$subscribe,store组合式写法

1.$subscribe订阅&#xff0c;监视vuex中数据得修改 2.localStorage里面穿的都是字符串&#xff0c;关掉浏览器数据还在 只能获取字符串&#xff0c;用ts语法写明&#xff0c;作为字符串使用 3.组合式写法

WAP短信格式解析及在Linux下用C语言实现

WAP短信格式解析及在Linux下用C语言实现 一、引言二、WAP短信格式概述三、WAP短信头的内容四、UDHI与WAP短信体的关系五、在Linux下用C语言解析WAP短信头及短信体内容一、引言 在移动通信领域,短信作为一种古老却稳定的通信方式,一直扮演着重要的角色。随着技术的发展,短信…

从 Coding (Jenkinsfile) 到 Docker:全流程自动化部署 Spring Boot 实战指南(简化篇)

前言 本文记录使用 Coding (以 Jenkinsfile 为核心) 和 Docker 部署 Springboot 项目的过程&#xff0c;分享设置细节和一些注意问题。 1. 配置服务器环境 在实施此过程前&#xff0c;确保服务器已配置好 Docker、MySQL 和 Redis&#xff0c;可参考下列链接进行操作&#xff1…

华为消费级QLC SSD来了

近日&#xff0c;有关消息显示&#xff0c;华为的消费级SSD产品线&#xff0c;eKitStor Xtreme 200E系列&#xff0c;在韩国一家在线零售商处首次公开销售&#xff0c;引起了业界的广泛关注。 尽管华为已经涉足服务器级别的SSD制造多年&#xff0c;但直到今年6月才正式推出面向…

visual studio连接sql server数据库

目录 1、为什么要建立连接2、在sql server中建立数据库3、visual studio连接sql server数据库4、学生信息管理系统页面布局5、添加事件逻辑 5.1 页面跳转5.2 读取学生信息5.3 查询学生信息5.4 修改学生信息5.5 删除学生信息5.6 添加学生信息 bilibili演示视频 github源码 1、…

HTML——30.视频引入

<head><meta charset"UTF-8"><title>视频引入</title></head><body><!--video:在网页中引入音频IE8以及之前版本不支持属性名和属性值一样&#xff0c;可以只写属性名src属性:指定视频文件路径&#xff0c;必须要有controls属…

基于Pytorch和yolov8n手搓安全帽目标检测的全过程

一.背景 还是之前的主题&#xff0c;使用开源软件为公司搭建安全管理平台&#xff0c;从视觉模型识别安全帽开始。主要参考学习了开源项目 https://github.com/jomarkow/Safety-Helmet-Detection&#xff0c;我是从运行、训练、标注倒过来学习的。由于工作原因&#xff0c;抽空…

如何使用MySQL的group_concat函数快速做关联查询?

当我们需要做一对多的关联查询时&#xff0c;会很容易想到用left join来实现。例如&#xff0c;现有country表和city表之间建立了一对多的关联关系。如果要展示各国家以及城市列表&#xff0c;会很容易想到以下SQL&#xff1a; SELECT country, city FROM country LEFT JOI…

plsql :用户system通过sysdba连接数据库--报错ora-01031

一、winR cmd通过命令窗口登录sys用户 sql sys/[password]//localhost:1521/[service_name] as sysdba二、输入用户名:sys as sysdba 三、输入密码:自己设的 四、执行grant sysdba to system; 再去PL/SQL连接就可以了

循环神经网络(RNN)入门指南:从原理到实践

目录 1. 循环神经网络的基本概念 2. 简单循环网络及其应用 3. 参数学习与优化 4. 基于门控的循环神经网络 4.1 长短期记忆网络&#xff08;LSTM&#xff09; 4.1.1 LSTM的核心组件&#xff1a; 4.2 门控循环单元&#xff08;GRU&#xff09; 5 实际应用中的优化技巧 5…