m3u8文件简介
在进行m3u8文件下载和播放前,要先对m3u8文件有一定的了解。
简介
- m3u8文件是一个基于文本的播放列表文件,通常用于流媒体播放。它本身是一个描述文件,并不是真正的可以播放的资源文件,它内部会包含多个URL地址等信息。
- 在播放m3u8文件的时候,播放器会首先加载m3u8文件本身,然后解析其中的内容,找寻到真正的可以播放的资源地址,然后进行一个顺序播放,类似播放列表一样,播放完一个资源后,再播放下一个资源。
- 每个可以播放的资源片段一般称为切片,用
ts
作为文件后缀
文件数据
一个标准的m3u8文件包含的内容,这里从网上找了一个公开的文件,并且后续demo里也会去下载和播放这个文件
http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8
这里显示了删除中间部分的内容:
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10, no desc
fileSequence0.ts
#EXTINF:10, no desc
fileSequence1.ts
#EXTINF:10, no desc
fileSequence6.ts
#EXTINF:10, no desc
fileSequence7.ts
#EXTINF:10, no desc
fileSequence8.ts
#EXTINF:1, no desc
fileSequence180.ts
#EXT-X-ENDLIST
#EXTM3U
:标识这是一个 M3U8 文件。#EXT-X-VERSION:3
:表示使用的版本号。#EXT-X-TARGETDURATION:10
:每个片段的最大持续时间为 10 秒。#EXT-X-MEDIA-SEQUENCE:0
:视频的片段序列号从 0 开始。#EXTINF:10.000000,
:表示接下来的资源片段的时长是 10 秒。通常最后一个资源片段的时长会小于10秒
,该文件的最后一个切片的时长为1秒
#EXTINF
后面跟着的信息代表一个资源切片文件(通常是.ts
格式的传输流文件)。该地址支持相对路径
和绝对路径
。在这个m3u8文件里描述的切片文件的路径就是相对路径
,相对于其自身的路径,如果要访问切片文件,需要去访问http://devimages.apple.com/iphone/samples/bipbop/gear1/fileSequence0.ts
,替换链接里最后一个文件名。#EXT-X-ENDLIST
:标识播放列表的结束。
m3u8文件的优点
m3u8文件的优点很多,这里只列举几个我们此次会用到的特性,其他的比如:自适应比特率、DRM、实时直播等特性,后续可以再继续慢慢使用。
- 分段传输。m3u8文件将一个大的资源切分为多个小的资源片段,可以更快的开始播放和加载剩余的数据
- 动态更新。可以手动更新播放列表内容,支持在一个m3u8文件中,资源切片文件路径可以指向不同的地址。另外还可以去通过动态插入新的资源片段地址,用来实现资源预加载、片头片尾和边播边载。
播放原理
目前鸿蒙的系统播放器media.AVPlayer
支持播放在线的m3u8格式的文件,简单描述一下播放器在播放m3u8文件的一个过程:
- 请求m3u8文件,解析资源片段和资源时长等数据,计算资源总时长等信息
- 根据用户seek的播放位置,决定要加载的切片文件
- 当一个资源切片文件播放完成后,自动读取下一个资源片段地址进行播放
- 当访问到
#EXT-X-ENDLIST
时,结束播放
下载和本地播放
本地播放
前面讲了播放器会在播放m3u8文件时,自动读取切片文件的地址,所以我们需要建立一个本地服务器
,并将已下载的m3u8文件和切片文件放在同一个目录下,通过本地服务器来访问本地文件。
- 在沙盒的
files
目录下创建一个新的目录m3u8cache
,用来保存所有的m3u8文件目录 使用开源库
polka
开启本地服务器,指定监听的端口
和主机名
/// 服务器对象 private server = polka() /** * 开启本地服务器 */ private startServer() { this.server.listen(22222, '127.0.0.1' message => { if (message.includes("error")) { console.log(`开启本地服务器失败:${message}`) this.retry() return } console.log(`开启本地服务器成功,${message}`) }) }
接收请求和处理响应
监听本地服务器访问
m3u8cache
目录下的所有请求,解析出对应的请求地址,生成本地文件地址,读取已下载的文件进行数据响应。在这里其实有很多可以做的操作,比如可以对数据流进行加解密
/** * 接收请求和处理响应结果 * 处理响应数据的过程可以做自定义的解密操作,在本地文件保存的时候进行加密,在读取的时候解密 */ private httpResponse() { this.server.get(`/m3u8cache/*`, (req, res) => { ///1. 获取请求地址,并转为本地文件地址 const path = req.path ///2. 读取文件数据,作为响应数据 const filePath = this.context.filesDir + path const access = fs.accessSync(filePath, fs.OpenMode.READ_ONLY) if (!access) { return } const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY) const stat = fs.statSync(file.fd) const arrayBuffer = new ArrayBuffer(stat.size) fs.readSync(file.fd, arrayBuffer) fs.close(file) res.end(arrayBuffer) }) }
- 本地服务器搭建完成后,在播放的时候就可以通过播放本地地址
http://127.0.0.1:22222/m3u8cache/dirname/filename.m3u8
即可进行播放,其中dirname
为每个m3u8文件所生成的目录名,filename.m3u8
为在该目录下的m3u8文件名,在该目录下,还会有很多的ts
文件。
下载
下载流程
- 用
request.downloadFile
封装一个异步下载任务,下载m3u8文件 新建两个模型用来存储m3u8数据
M3U8Model
和切片数据TSModel
,下载m3u8文件完成后,逐行解析出所有的切片数据。简化后的模型定义如下:export class M3U8Model { // 播放列表数据 playlist: TSModel[] = [] // 资源时长 totalDuration: number = 0 // 远端文件地址 netUrl: string = "" // 目录名 dirName: string = "" // 文件名 fileName: string = "" // 下载状态 downloadState: DownloadState = DownloadState.wait // 下载进度,0-1 percent: number = 0 } export class TSModel { // 目录名 dirName: string = "" // 文件名 fileName: string = "" // 片段时长 duration: number = 0 // 远端文件地址,相对路径 netUrl: string = "" // 下载进度,单位字节 processed: number = 0 // 下载状态 downloadState: DownloadState = DownloadState.wait // 资源大小 fileSize: number = 0 // 下载进度,0-1 percent: number = 0 }
- 开始下载时,从
playlist
中查询最新一个未下载完成的ts
资源,使用request.agent
进行下载。 - 每个
ts
文件下载完成后,查询下一个ts
,直到所有的ts
文件下载完成后,更新M3U8Model
下载状态为已完成
文件存储
当一个新的m3u8文件开始下载时,截取远程地址的getLastSegment
作为m3u8文件的文件名,将该文件名md5
之后,在上文提到的本地服务器的根目录m3u8cache
下创建一个对应的文件夹,后续m3u8文件和其下所有的ts文件,都将存储在这个目录下。
下载进度的计算
因为在下载ts
文件的时候,只能获取到当前的文件的已下载字节数,所以可以用一个取巧的方法计算进度,将所有的ts
文件大小默认为相同的,所以下载进度=每个ts的进度相加/总的ts个数
。
同时在'progress'
的回调里,缓存ts
的processed
,记录已下载的字节数。
this.downloadingTask.on('progress', progress => {
if (this.currentDownloadTsData && this.currentDownloadM3U8Data) {
this.currentDownloadTsData.downloadState = DownloadState.downloading
this.currentDownloadM3U8Data.downloadState = DownloadState.downloading
if (progress.sizes.length > 0) {
// 文件大小单位为字节,如果是断点下载,此时的sizes为剩余的文件大小
// 文件大小在首次下载时设置,后续不再更新
let sizes = progress.sizes[0]
if (sizes === -1) {
console.log(`没有获取到文件大小`)
}
if (this.currentDownloadTsData.fileSize === 0) {
this.currentDownloadTsData.fileSize = sizes
} else {
sizes = this.currentDownloadTsData.fileSize
}
// 当前下载进度,需要基于完整文件的大小计算,和累计下载内容大小
const lastProcessed = sizes - progress.sizes[0]
let processed = progress.processed
if (lastProcessed) {
processed += lastProcessed
}
this.currentDownloadTsData.percent = processed / sizes
/// 根据ts片段每个进度计算总的进度
let percent = 0
this.currentDownloadM3U8Data.playlist.forEach(item => percent += item.percent)
/// 总进度
this.currentDownloadM3U8Data.percent = percent / this.currentDownloadM3U8Data.playlist.length
this.currentDownloadTsData.processed = processed
console.log(`progress:fileName: ${this.currentDownloadTsData.fileName},processed: ${processed}, size: ${sizes}}`)
}
}
})
断点下载
在下载ts文件的过程中,如果因为暂停或者网络波动导致下载失败时,在下次进行下载时,就只需要从未下载的部分开始下载,下载完成后,将多个下载文件进行合并。
在使用request.agent
开始下载时,设置下载配置:
saveas
配置下载配置的
saveas
字段时,需要判断当前的TSModel
的processed
字段是否大于0
,processed
缓存了当前ts文件的已下载字节数,代表本地已经存在了一个下载文件,再次下载时,就需要修改保存的文件名,修改后的格式为:xxx_123.ts
,123
即processed
的值。begins
配置下载的开始位置,即当前的
TSModel
的processed
值
下载完成后:
- 查询当前目录下的所有以
xxx
开头的文件 - 截取每个文件
xxx_123.ts
的123
,按照升序进行排序 - 遍历所有的文件进行文件合并
- 删除多余的文件
最后
在这里简单介绍了下载一个完整的m3u8文件之后,通过开启本地服务器来进行播放。后续有几个相关的功能可以继续去实现:
预加载
提前下载好m3u8文件的前几个ts资源,当用户播放时,达到一个秒播的效果。
片头片尾
基于m3u8文件本身的动态性,可以通过编辑m3u8文件,将片头片尾作为ts片段插入到开头或者结尾,并且设置其路径为绝度路径
边播边存
在本地服务器接收到请求时下载对应的资源并返回。
Demo
demo地址:https://gitee.com/brighton/m3u8download.git
下载类:CDownloader.ets
本地服务类:CHTTPServer.ets
模型:M3U8Model.ets
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。