m3u8tomp4
- 下载m3u8分片
- 合并分片
- 使用
ffmpeg
转为 mp4 文件
技术使用:
Bun + ffmpeg
- 使用
fetch
请求.m3u8
分片文件 - 使用
Bun.write
保存每个分片 - 使用
Bun.file().writer
创建增量写入writer: FileSink
- 循环读取
Bun.file
每个分片,并通过writer.write
- 最后,通过
ffmpeg -i output.ts -c copy output.mp4
把合并完成的视频转换为mp4
import { file, write, readableStreamToArrayBuffer } from 'bun'; import { readdir, unlink } from 'node:fs/promises'; const BASE_URL = ''; // 追加写入 // async function test() { // const dirs = await getDownloadParts(); // const outputFile = file('./test/text_copy.txt'); // const writer = outputFile.writer(); // for (const dir of dirs) { // writer.write(`${dir}\r\n`); // await writer.flush(); // } // console.log('写入完成'); // await writer.end(); // writer.unref(); // } // 追加写入 // const outputFile = file('./test/text_copy.txt'); // const writer = outputFile.writer({ highWaterMark: 1024 }); // for (let i = 0; i < 1024 * 1024; i++) { // writer.write(`${i}\r\n`); // // await writer.flush() // } // console.log('写入完成'); // const text = await outputFile.text(); // console.log('text => ', text.split('\r\n').length); // // await writer.end(); // 覆盖写入 // for (let i = 0; i < 100; i++) { // await write(file('./test/text_copy.txt'), i); // await Bun.sleep(500); // } run(); async function run() { await fetchM3u8(`${BASE_URL}/20240325/ihRFBhF0/1500kb/hls/index.m3u8`); await donwloadM3u8Part(); await mergeParts(); m3u82mp4(); } async function fetchM3u8(url) { if (!url?.endsWith('.m3u8')) throw new Error('url is not a m3u8 file'); const text = await fetch(url).then((res) => res.text()); const matchs = text.match(/(.*\.ts)/g); await write(file('./test/text.txt'), matchs.join('\r\n')); console.log('m3u8 解析完成'); } async function donwloadM3u8Part() { const bunFile = file('./test/text.txt'); const text = await bunFile.text(); const texts = text.split('\n'); const tsList = Array.from({ length: texts.length }, (_, i) => ({ index: i + 1, src: texts[i] })); const dirs = await getDownloadParts(); const len = dirs.length; for (const { src, index } of tsList) { // 已下载 if (index > len - 1) { const videoBuf = await fetch(`${BASE_URL}${src}`).then((res) => res.arrayBuffer()); await write(file(`./video/${index}.ts`), videoBuf); } const percent = parseFloat(((index / tsList.length) * 100).toFixed(2)); console.clear(); console.log('下载进度 => ', percent + '%'); } console.log('m3u8 所有分片下载完成'); } // 合并输出到一个.ts文件 async function mergeParts() { // output const dirs = await getDownloadParts(); // console.log('dirs => ', dirs); const outputFile = file('./output/output.mp4'); const writer = outputFile.writer(); let count = 0; for (const dir of dirs) { const bunFile = file(`./video/${dir}`); const buf = await readableStreamToArrayBuffer(bunFile.stream()); writer.write(buf); writer.flush(); count++; console.log('合并进度 => ', parseFloat(((count / dirs.length) * 100).toFixed(2)) + '%'); } console.log('增量写入完成'); writer.end(); writer.unref(); } // 获取已下载的分片 async function getDownloadParts() { let dirs = await readdir('./video'); dirs = dirs.filter((dir) => dir.endsWith('.ts')); dirs.sort((a, b) => { a = a.split('.')[0].padStart('0', 8); b = b.split('.')[0].padStart('0', 8); return a - b; }); return dirs; } async function m3u82mp4() { // ffmpeg -i output.ts -c:v libx265 -crf 0 -r 60 copy output.mp4 await Bun.$`ffmpeg -i output.ts -c copy output.mp4`; }
也可以不合并成一个文件,使用
Http Range
,前端使用MediaSource
添加分片播放
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现