使用HLS插件播放m3u8格式的切片视频

一、需求

后端提供视频接口,采用 HLS协议 ,对视频进行分片,并将索引 .m3u8 和分片 .ts 全部上传到MinIO,MinIO不保留原始完整视频文件。
前端先调用后端获取索引文件接口获取视频索引,再根据返回的索引调用后端获取分片文件接口获取视频分片信息.ts,最后播放这些分片信息。

索引文件返回索引格式如下:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:286
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-DISCONTINUITY
#EXTINF:16.033333,
000.ts
#EXTINF:13.000000,
001.ts
#EXTINF:285.900000,
002.ts
#EXTINF:257.900000,
003.ts
#EXTINF:4.633333,
004.ts
#EXTINF:163.733333,
005.ts
#EXTINF:4.666667,
006.ts
#EXTINF:0.800000,
007.ts
#EXT-X-ENDLIST

二、坑

1、拼接url

使用正则替换的时候一定要使用正确的正则,不然替换的时候会替换成2行,导致请求404,类似于下面这样:/000.ts 直接跑下一行去了,路径错误。

#EXTINF:4.666667,
http://XXX/getChunkFile/a7e240cf7350445e9bfa4b0738e4fd33
/000.ts
#EXTINF:4.666667,
2、带上token 授权

本例中采用简单实现,也可以使用自定义的 AxiosLoader

三、实现代码

import Hls from 'hls.js';

let video = null;
let hls = null;
let playlistBlobUrl = null;

// 处理m3u8文本,替换相对路径为完整URL
const processM3u8 = (playlist, baseUrl) => {
  // 匹配TS文件路径的正则(简单匹配以.ts结尾的行)
  // ^ 与 $ 匹配每一行的行首/行尾,不以#或空白字符开头的行
  const tsRegex = /^([^#\s]+\.ts)$/gm;

  // 替换相对路径为完整URL
  return playlist.replace(tsRegex, (match) => {
    return new URL(match, baseUrl).href;
  });
}
const initPlayer = async () => {
  try {
    // 获取索引文件(m3u8 文本)
    const indexResponse = await api_getIndexFile(props.videoId);
    if (!indexResponse || !indexResponse.data) {
      throw new Error('无法获取索引文件(M3U8)');
    }

    // 构建播放列表(将相对TS路径替换为完整URL)
    const baseUrl = window.location.origin + `/getChunkFile/${props.videoId}/`;
    // 处理m3u8文本
    const processedM3u8 = processM3u8(indexResponse.data, baseUrl);
    // 创建 blob url 供 hls.js 或原生播放使用
    const blob = new Blob([processedM3u8], { type: 'application/x-mpegURL' });
    playlistBlobUrl = URL.createObjectURL(blob);

    // 优先使用 hls.js 转码播放(更兼容大多数浏览器)
    if (Hls.isSupported()) {

      const token = localStorage.getItem('token');

      hls = new Hls({
        // 可扩展的 hls.js 配置(需要时调整)
        enableWorker: true,
        lowLatencyMode: false,
        // 使用 xhrSetup 在每次 XHR 请求上添加自定义 header(例如 Authorization)
        xhrSetup: function (xhr) {
          try {
            if (token) {
              xhr.setRequestHeader('Authorization', token);
            }
            // 如果需要其他自定义 header,可在此添加
          } catch (e) {
            // 安静处理 header 设置错误
          }
        },
        // 可选:配置加载器,也可以使用自定义的 AxiosLoader
        loader: Hls.DefaultLoader,
      });

      // 更详细的错误与加载事件,便于调试分片请求失败
      hls.on(Hls.Events.ERROR, function (event, data) {
        console.error('HLS错误:', data);
        if (data.fatal) {
          switch (data.type) {
            case Hls.ErrorTypes.NETWORK_ERROR:
              console.error('网络错误,尝试恢复...');
              hls.startLoad();
              break;
            case Hls.ErrorTypes.MEDIA_ERROR:
              console.error('媒体错误,尝试恢复...');
              hls.recoverMediaError();
              break;
            default:
              // 无法恢复的错误,销毁播放器
              destroyPlayer(hls, video);
              break;
          }
        }
      });
      hls.on(Hls.Events.FRAG_LOAD_ERROR, function (event, data) {
        console.error('hls.js fragment load error', data);
      });
      hls.on(Hls.Events.LEVEL_LOAD_ERROR, function (event, data) {
        console.error('hls.js level load error', data);
      });

      hls.loadSource(playlistBlobUrl);
      hls.attachMedia(video);
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      // 在支持原生 HLS 的浏览器上直接赋值(如 Safari)
      video.src = playlistBlobUrl;
    }

  } catch (err) {
    console.error('初始化播放器失败:', err);
  }
}
// 销毁播放器
const destroyPlayer=(hls, videoElement) => {
  if (hls) {
    hls.destroy();
  }
  videoElement.pause();
  videoElement.src = '';
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容