前因
最近做系统时,笔者发现在直接将文件流返回给前端的情况下,前端播放视频会有问题,无法拖动快进快退,也无法知道视频长度,不同设备或系统版本上甚至无法播放。在深入探索了解原因后增长了见识,本文做一个总结记录。
探索
视频地址与普通地址有何不同?
笔者一开始在系统里是直接在后端读写文件流用于管理用户上传的文件。相当于简单地实现了oss文件管理。一般情况下地上传下载都没有问题。后来一个项目遇到需要播放视频的需求,那么自然地复用了这套方案,没曾想,这就是问题开始的地方。
一开始,上传的视频通过下载接口去获取文件流下载或是浏览器根据文件名自动预览播放都还算可以正常播放,只是笔者没有注意无法拖动快进快退。
紧接着,前端同事在某设备端上运行时,发现了问题:有些视频无法播放,有些视频无法拖动改变进度。
经过不断对比排查,笔者根据这些现象做了排除法:排除设备的系统和浏览器内核版本较老,另外找的网络上的视频地址能正常播放能控制播放进度,那么最后怀疑对象就是下载链接了。
视频地址与普通地址有什么不同吗?
笔者带着这个疑问,查阅了一些资料和对比网上公开的视频链接与系统的下载接口。发现视频地址并没有不同,都是后台返回文件流。(这里暂时不考虑后台视频推流m3u8这些流媒体格式)
但也从对比分析中发现,视频地址的响应头中有不一样的特殊请求头和响应头。
浏览器前端请求
其实如果认真思考一下,也能很快发现问题。
一个视频动辄几十上百M甚至大一点就是多少个G,这么大的文件流,难道前端浏览器播放时是必须要全部下载下来才能播放?如果按普通文件流,能够一边下载一边播放,那也只能从头开始,不能随意控制进度。而且由于文件流未下载完,也不清楚整体长度,还有这只是单个http连接,如果断开,岂不是又只能从头开始播放?
通过思考这些问题,在没有专门了解过http协议和浏览器等客户端的某些特殊应用场景的使用方法前,我们还是能大概地推测出一些问题和解决方案。
笔者在解决问题过程中打开浏览器控制台,观察到视频地址虽然与普通地址本质上一样,但其与后台交互过程中,有几个http header
明显不同。
- Range
range用于客户端请求时确定需要从服务器下载的文件流的区段。一般以bytes为单位,如下载视频的第600-1200个字节:Range: bytes=600-1200
range示例:
ini
Range:bytes=0-500
表示下载从0到500字节的文件,即头500个字节 ,[0-500]前闭后闭。0<=range<=500
Range:bytes=501-1000
表示下载从500到1000这部分的文件,单位字节
Range:bytes=-500
表示下载最后的500个字节
Range:bytes=500-
表示下载从500开始到文件结束这部分的内容
Range:bytes=500-600,700-1000
表示下载这两个区间的内容
一般的静态文件服务器都支持以上示例,但具体支持情况需视后台服务器而定。
- If-Range
If-Range用于判断实体是否发生改变,如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体.
If-Range 可以使用 Etag 或者Last-Modified 返回的值。当没有 ETag 却有 Last-modified 时,可以把 Last-modified 作为 If-Range 字段的值。 如果请求报文中的 Etag与服务器目标内容的 Etag相等,即没有发生变化,那么应答报文的状态码为 206。如果服务器目标内容发生了变化,那么应答报文的状态码为 200。
- Content-Range
这个是服务器的响应头,服务器按客户端的请求Range读取到文件片段后,会在响应头中设置此值。如Content-Range: bytes 600-1200/2000
代表响应的范围格式:数据为bytes类型,读取的是600-1200这个片段,总文件长度为2000。
- Last-Modified & Etag
这两个头是与上述If-Range相对应的,用于确定文件是否修改,此处不做详解。
浏览器每次range获取一个区块都会有一个http请求,服务器处理正常响应时,响应码为206,这样浏览器前端就能明确知道请求的区块成功了。
而快进快退功能,通过观察控制台,轻松地发现,其实就是每个范围的range区块都发送的一个http请求,那么快进到某个点时,就是计算出该点的开始位置的大小索引值,然后按一定宽度请求到后台,就能从这点继续开始播放了。
服务器端实现
经过上述探索http的请求与响应头,我们知道了实现http视频地址的如何分段下载播放,如何快进快退的原理。其实这就是客户端和服务器的交互中的断点续传的工作原理。
此处我们再简要说明相对应的服务器端如何去处理这些断点续传请求。
以笔者使用的eggjs
框架为例,笔者在框架中找到了egg-static
插件,它是专门用于作为框架中的静态文件服务器模块。
在egg-static
中的主要逻辑是对koa的插件进行封装。在这里找到了koa-static-cache
插件和koa-range
插件。
koa-static-cache
中间件主要是用于缓存文件在内存中,以加快静态文件访问速度,此处不作详解。
koa-range
插件就是本文所关注的静态文件服务器的断点续传分段式下载大资源的实现核心。koa-range是koa的中间件,它的主体功能就是从header中读取客户端的range等参数,去读取对应的文件流的相应分段。其中,实现大文件分段文件流切分提取功能的,有一个stream-slice
包,它是一个非常小巧的包,主要就是实现了一个transform转换流,然后从中提取具体范围的分段buffer,其他数据都丢弃。
eggjs
是国内阿里开源的nodejs框架,对熟悉node的开发者来说能够理解上述说到的对stream流
进行切割。但其他语言开发者来说,其内里原理也是一致的。如php使用fseek
定位文件读取指针到区块的开始位置,go语言的file.ReadAt
等都可以实现文件分块读取。
注意:这些后端处理方式,不管是流式读取文件,还是指定读取位置等方式,都不可能是将整个文件读取入内存,然后再切割的,否则一个大文件将直接撑爆内存。
总结
- 在基础http请求之上,为了实现视频的快进快退功能,或者说断点续传功能,需要客户端和服务器端按http协议的要求使用range等header来进行交互。
- 这些是http协议与客户端和服务器端实现的相互配合与实现完成的,我们在实际工作中接触到知识盲区时,应该深入了解一些基础理论知识,同时配合前后端源码实现加深了解。
- 本文仅是笔者工作中遇到的问题进而探索的概况总结,不涉及技术细节。