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文件的一个过程:

  1. 请求m3u8文件,解析资源片段和资源时长等数据,计算资源总时长等信息
  2. 根据用户seek的播放位置,决定要加载的切片文件
  3. 当一个资源切片文件播放完成后,自动读取下一个资源片段地址进行播放
  4. 当访问到#EXT-X-ENDLIST时,结束播放

下载和本地播放

本地播放

前面讲了播放器会在播放m3u8文件时,自动读取切片文件的地址,所以我们需要建立一个本地服务器,并将已下载的m3u8文件和切片文件放在同一个目录下,通过本地服务器来访问本地文件。

  1. 在沙盒的files目录下创建一个新的目录m3u8cache,用来保存所有的m3u8文件目录
  2. 使用开源库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}`)
        })
      }
  3. 接收请求和处理响应

    监听本地服务器访问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)
        })
      }
  4. 本地服务器搭建完成后,在播放的时候就可以通过播放本地地址http://127.0.0.1:22222/m3u8cache/dirname/filename.m3u8即可进行播放,其中dirname为每个m3u8文件所生成的目录名,filename.m3u8为在该目录下的m3u8文件名,在该目录下,还会有很多的ts文件。

下载

下载流程

  1. request.downloadFile封装一个异步下载任务,下载m3u8文件
  2. 新建两个模型用来存储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
    }
  3. 开始下载时,从playlist中查询最新一个未下载完成的ts资源,使用request.agent进行下载。
  4. 每个ts文件下载完成后,查询下一个ts,直到所有的ts文件下载完成后,更新M3U8Model下载状态为已完成

文件存储

当一个新的m3u8文件开始下载时,截取远程地址的getLastSegment作为m3u8文件的文件名,将该文件名md5之后,在上文提到的本地服务器的根目录m3u8cache下创建一个对应的文件夹,后续m3u8文件和其下所有的ts文件,都将存储在这个目录下。

下载进度的计算

因为在下载ts文件的时候,只能获取到当前的文件的已下载字节数,所以可以用一个取巧的方法计算进度,将所有的ts文件大小默认为相同的,所以下载进度=每个ts的进度相加/总的ts个数

同时在'progress'的回调里,缓存tsprocessed,记录已下载的字节数。

    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字段时,需要判断当前的TSModelprocessed字段是否大于0processed缓存了当前ts文件的已下载字节数,代表本地已经存在了一个下载文件,再次下载时,就需要修改保存的文件名,修改后的格式为:xxx_123.ts,123processed的值。

  • begins

    配置下载的开始位置,即当前的TSModelprocessed

下载完成后:

  1. 查询当前目录下的所有以xxx开头的文件
  2. 截取每个文件xxx_123.ts123,按照升序进行排序
  3. 遍历所有的文件进行文件合并
  4. 删除多余的文件

最后

在这里简单介绍了下载一个完整的m3u8文件之后,通过开启本地服务器来进行播放。后续有几个相关的功能可以继续去实现:

  • 预加载

    提前下载好m3u8文件的前几个ts资源,当用户播放时,达到一个秒播的效果。

  • 片头片尾

    基于m3u8文件本身的动态性,可以通过编辑m3u8文件,将片头片尾作为ts片段插入到开头或者结尾,并且设置其路径为绝度路径

  • 边播边存

    在本地服务器接收到请求时下载对应的资源并返回。

Demo

demo地址:https://gitee.com/brighton/m3u8download.git

下载类:CDownloader.ets

本地服务类:CHTTPServer.ets

模型:M3U8Model.ets

三方库

polka


Taeyss
674 声望68 粉丝