M3U8 格式是一种视频流媒体格式,基于 HTTP Live Streaming(HLS)协议的视频文件格式,与传统的视频格式不同,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 格式文件下载工具,效果如下: