一、需求
后端提供视频接口,采用 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 = '';
}