网站开发之m3u8视频解析

生活日记 工作心得
7
发表时间: 编辑:顾永胜来源:0513.city标签:原创

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(!srcreturn false;
        try{
            return new URL(srclocation.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.ERRORfunction (_data) {
            if (data.fatal) {
                hls.destroy();
                const idx = hlsInstanceList.indexOf(hls);
                if (idx > -1hlsInstanceList.splice(idx1);
            }
        });
        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(videoDomnewUrl) {
        const idx = hlsInstanceList.findIndex(item => item.media === videoDom);
        if(idx !== -1){
            hlsInstanceList[idx].destroy();
            hlsInstanceList.splice(idx1);
        }
        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
    // };
})();