M3U8视频播放及下载

M3U8 格式是一种视频流媒体格式,基于 HTTP Live Streaming(HLS)协议的视频文件格式,与传统的视频格式不同,M3U8 视频格式将整个视频分成多个小片段进行传输,这些小片段可以根据网络情况自动调节其质量和大小。这种方式使得 M3U8 视频格式非常适合在网络环境不稳定或带宽不足的情况下播放视频。

M3U8 视频下载

 

既然谈及流媒体,或许大家也该清楚,常规的流媒体文件很难以常规途径使用及下载,举个例子:
传统的视频播放:


<video  width="640" height="264" autoplay muted controls>
        <source src="./static/01.mp4" type="video/mp4"></source>
</video>

 

如果采用常规的压缩视频,例如 MP4,毫无疑问,一切正常,但是,如果将 m3u8 格式视频以此种方法展示,结果是 “无法播放”,什么原因呢?我们先来看看 M3U8 格式视频内容:

不难发现,M3U8 格式视频内容是以文本的形式展示,它不是视频或音乐文件本身,它是一个包含媒体播放器指令的文本文件,所以说,以传统的方式无法正常播放器内容,那它该如何播放呢?
M3U8 格式视频:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>M3U8 视频播放</title>
    <link href="https://vjs.zencdn.net/7.11.4/video-js.css" rel="stylesheet" />
</head>
<body>
    <video id="my-video" class="video-js" controls muted preload="auto" width="640" height="264" data-setup='{"techOrder": ["html5"]}'>
        <source src="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" type="application/x-mpegURL">
        <p class="vjs-no-js">
            要查看此视频,请启用 JavaScript 并考虑升级到支持 HTML5 视频的 Web 浏览器
        </p>
    </video>
    <!-- 省略其他内容 -->
    <script src="https://vjs.zencdn.net/7.11.4/video.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            var player = videojs('my-video');
            player.play();
        });
</script>
</body>
</html>

 

这是一个 M3U8 格式视频播放的 demo,与传统的视频播放不同的是,该案例中引用了一个依赖库 video.js,通过对 m3u8 格式视频进行转化从而实现播放。

或许你会问道:“既然这么麻烦,那它能否下载呢?”,答案是肯定能下载,你要知道,计算机只是一个工具,创建规则的同时意味着我们可以逆推出其原型,M3U8 格式视频同样可以下载。

M3U8 视频下载:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>M3U8 视频下载</title>
    <style>
        body {
            text-align: center;
            background-color: aliceblue;
        }
        input,button {
            line-height: 30px;
            padding: 10px;
            cursor: pointer;
        }
        input {
            width: 30vw;
        }
        .title {
            margin: 100px 0;

        }
</style>
</head>

<body>
    <h1 class="title">M3U8 视频下载</h1>
    <input type="text" placeholder="请输入 M3U8 视频地址" id="m3" onblur="handleblur()" onfocus="handleblur()" onchange="handleblur()">
    <button id="playDownload">下载</button>
    <p id="textas">例如:https://vip.lz-cdn5.com/20220914/42199_41e5bfff/index.m3u8</p>

    <script>
        const m3 = document.getElementById('m3')
        const textas = document.getElementById('textas')
        let targetUrl
        let targetEndWithUrl = ''
        let matches

        function handleblur(){
            targetUrl = m3.value || 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'
            const pattern = /\/([^\/]+)$/; // 正则表达式模式
            matches = pattern.exec(targetUrl); // 提取匹配结果
            if (matches && matches.length > 1) {
            const filename = matches[1]; // 获取匹配结果中的文件名
            targetEndWithUrl = filename // 输出: index.m3u8
        }
        }

        

        // 开始下载按钮
        document.getElementById('playDownload').onclick = () => {
            console.log(m3.value);
            if (!m3.value) return alert('请输入 M3U8 视频地址')
            new Download(targetUrl)
        }


        class Download {

            // 最大下载队列数
            maxTask = 1

            // 当前下载数
            nowDownTaskIndex = 0

            // 下载目标地址
            downLoadUrl = null

            // 下载分片地址
            downLoadSplices = []

            // 下载暂存数组
            downLoadTemps = []

            constructor(url) {
                this.downLoadUrl = url
                this.analysisUrlToSplice(url)
            }


            // 调用解析
            analysisUrlToSplice(targetUrl) {
                this.getFetchUrlStream(targetUrl).then(async res => {
                    this.downLoadSplices = await this.analysisMultiUrl(res)
                    this.maxTask = this.downLoadSplices.length
                    if (this.maxTask)
                        //  开始下载分片数据
                        this.analysisList()
                })
            }


            // 传入 url 判断是多个地址  还是  单个地址
            getFetchUrlStream(url) {
                return new Promise((suc, rej) => {
                    fetch(url)
                        .then(response => {
                            if (!response.ok) {
                                throw new Error(`请求失败,状态码:${response.status}`);
                            }
                            return response.text();
                        })
                        .then(text => {
                            let analysisList = text.split('\n')
                            analysisList.forEach(x => {
                                if (x.endsWith('.m3u8')) {

                                    suc(x)
                                }
                            })
                        })
                        .catch(error => {
                            console.error('发生错误:', error);
                            rej('未找到解析列表')
                        });
                })
            }


            // 多个地址解析函数
            analysisMultiUrl(endWithUrl) {
                let TempUrl = this.downLoadUrl.replace(targetEndWithUrl, endWithUrl)
                return new Promise((suc, rej) => {
                    fetch(TempUrl)
                        .then(response => {
                            if (!response.ok) {
                                throw new Error(`请求失败,状态码:${response.status}`);
                            }
                            return response.text();
                        })
                        .then(text => {
                            let result = []
                            text.split('\n').forEach(x => {
                                if (x.endsWith('.ts')) {
                                    result.push(TempUrl.replace(targetEndWithUrl, x))
                                }
                            })
                            suc(result);
                        })
                        .catch(error => {
                            rej('多个地址解析函数发生错误:' + error);
                        });
                })
            }


            // 解析多个分片数据
            analysisList() {
                // 下载 .ts 文件并存储到数组中
                let Task = () => {
                    return new Promise((suc, rej) => {
                        // console.log(this.nowDownTaskIndex);
                        fetch(this.downLoadSplices[this.nowDownTaskIndex])
                            .then(response => {
                                if (!response.ok) {
                                    this.nowDownTaskIndex = 0
                                    rej(`下载失败,状态码:${response.status}`);
                                }
                                return response.arrayBuffer();
                            })
                            .then(arrayBuffer => {
                                this.nowDownTaskIndex += 1
                                suc(arrayBuffer)
                            })
                            .catch(error => {
                                console.log(error);
                                this.nowDownTaskIndex = 0
                                rej('解析错误', error)
                            })
                    })
                }

                Task().then(res => {
                    this.downLoadTemps.push(res)
                    textas.innerHTML = `下载中第${this.nowDownTaskIndex}段,总共${this.downLoadSplices.length}`
                    // console.log(`下载中第${this.nowDownTaskIndex}段,总共${this.downLoadSplices.length}`);
                    if (this.nowDownTaskIndex >= this.downLoadSplices.length) {
                        // 计算合并后的总长度
                        const totalLength = this.downLoadTemps.reduce((acc, buffer) => acc + buffer.byteLength, 0);
                        textas.innerHTML = '下载完成!'
                        setTimeout(()=>{
                            textas.innerHTML = ''
                            m3.value = ''
                        },5000)
                        // console.log(totalLength);
                        // 创建新的 ArrayBuffer
                        const mergedBuffer = new ArrayBuffer(totalLength);

                        // 使用视图将各个 TypedArray 的数据复制到 mergedBuffer
                        const mergedView = new Uint8Array(mergedBuffer);
                        let offset = 0;
                        this.downLoadTemps.forEach(buffer => {
                            const view = new Uint8Array(buffer);
                            mergedView.set(view, offset);
                            offset += buffer.byteLength;
                        });
                        // // 将数组中的 .ts 文件数据合并成一个完整的视频文件
                        const mergedData = new Uint8Array(mergedView);
                        // // 将合并后的数据保存为一个完整的视频文件(使用 File API)
                        const mergedBlob = new Blob([mergedData], { type: 'video/mp2t' });
                        const file = new File([mergedBlob], 'video.ts', { type: 'video/mp2t' });

                        // 保存文件到本地(下载)
                        const downloadLink = document.createElement('a');
                        downloadLink.href = URL.createObjectURL(file);
                        downloadLink.download = file.name;
                        downloadLink.click();


                        this.maxTask = 0
                        this.nowDownTaskIndex = 0
                        this.downLoadUrl = null
                        this.downLoadSplices = []
                        this.downLoadTemps = []
                        return;

                    }
                    this.analysisList()

                })

            }
        }
</script>
</body>

</html>

这是一个利用纯 js 编写的 M3U8 格式文件下载工具,效果如下:

M3U8 视频播放及下载

 

© 版权声明

☆ END ☆
喜欢就点个赞吧
点赞0 分享
图片正在生成中,请稍后...