M3U8是苹果公司发明的HLS流媒体协议的索引文件。它并不是真正的视频文件,只是一份文本播放清单,里面记录着一个个短视频分片的地址,本身不包含视频画面数据。
普通浏览器直接打开m3u8链接,地址栏也无法播放视频,只会把这个文本索引文件下载保存到本地。
网页标准的video标签,原生并不支持直接播放m3u8。
部分安卓手机浏览器看上去可以播放m3u8链接,并不是网页video标签拥有播放能力,而是浏览器识别链接后,调用了手机系统自带播放器来解码播放,属于系统能力接管,并不是网页本身的能力。
电脑端Chrome、Edge浏览器打开m3u8链接,只会直接下载索引文件,不会自动播放视频。
想要在网页页面内正常播放m3u8视频,需要引入hls.js这类第三方JS库,解析m3u8索引,读取视频分片,再交给video标签渲染播放。只有苹果iOS设备是例外,它的浏览器内核原生支持M3U8,可以直接在video标签中使用。
现在各大视频网站、网课、直播大量使用M3U8格式,视频被切割成几秒一段的小碎片,可以根据网络好坏自动切换清晰度,实现边下载边播放。
下面是一个动态加载hls.js的代码,先检查浏览器中的video是否支持播放m3u8格式,如果不支持才下载hls.js接管video。
动态加载script,配合离线缓存workbox时,如果配置不当会有奇怪的问题。动态加载的JS要加入到workbox.precaching.precacheAndRoute里面。
JS缓存模式可以用workbox.strategies.StaleWhileRevalidate。
(function(){
const hlsInstanceList = [];
let loadPromise = null;
function loadHlsLib(){
if(window.Hls && window.Hls.isSupported){
return Promise.resolve(window.Hls);
}
if(loadPromise){
return loadPromise;
}
loadPromise = new Promise((resolve)=>{
const script = document.createElement('script');
script.src = "/static/js/hls/hls.min.js";
script.onload = ()=>{
if(window.Hls && window.Hls.isSupported){
resolve(window.Hls);
}else{
loadPromise = null;
resolve(null);
}
};
script.onerror = ()=>{
loadPromise = null;
resolve(null);
};
document.head.appendChild(script);
});
return loadPromise;
}
function isM3u8Url(src){
if(!src) return false;
try{
return new URL(src, location.href).pathname.endsWith('.m3u8');
}catch(e){
return false;
}
}
function hasNativeHlsSupport(videoDom){
return !!videoDom.canPlayType('application/vnd.apple.mpegurl');
}
async function setupHlsIfNeeded(videoDom) {
const src = (videoDom.src || '').trim();
if (!isM3u8Url(src) || hasNativeHlsSupport(videoDom)) return;
const Hls = await loadHlsLib();
if (!Hls || !Hls.isSupported()) return;
const m3u8Url = videoDom.src;
videoDom.removeAttribute('src');
videoDom.load();
const hls = new Hls();
hls.loadSource(m3u8Url);
hls.attachMedia(videoDom);
hls.on(Hls.Events.ERROR, function (_, data) {
if (data.fatal) {
hls.destroy();
const idx = hlsInstanceList.indexOf(hls);
if (idx > -1) hlsInstanceList.splice(idx, 1);
}
});
hlsInstanceList.push(hls);
}
function initAllHlsVideo() {
document.querySelectorAll('video').forEach(el => {
setupHlsIfNeeded(el).catch(()=>{});
});
}
function destroyAllHls() {
hlsInstanceList.forEach(ins => {
try { ins.destroy(); } catch (e) {}
});
hlsInstanceList.length = 0;
}
async function changeVideoSource(videoDom, newUrl) {
const idx = hlsInstanceList.findIndex(item => item.media === videoDom);
if(idx !== -1){
hlsInstanceList[idx].destroy();
hlsInstanceList.splice(idx, 1);
}
videoDom.src = newUrl;
await setupHlsIfNeeded(videoDom).catch(()=>{});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAllHlsVideo);
} else {
initAllHlsVideo();
}
window.addEventListener('beforeunload', destroyAllHls);
// window.__HlsVideoHelper__ = {
// scan: initAllHlsVideo,
// destroyAll: destroyAllHls,
// setSource: changeVideoSource
// };
})();