m3u8转mp4工具开发
转眼已是正月十六,小君在这里给大家拜个晚年,祝大家新的一年身体安康,诸事顺利。上来先安利一波最近抽时间完成的m3u8转mp4工具,开发语言是node.js
。Github地址:https://github.com/supervergil/node-m3u8-to-mp4,觉得好用不要忘记star一波^_^!我已经打包上传到npm上,你可以用npm install node-m3u8-to-mp4 -S
命令进行安装。本文和大家分享一下开发体会。

用node
还是deno
本来我是想试用一下deno
开发这个转换工具的,我也确实在github上创建了一个名为deno-m3u8-to-mp4的仓库,无奈由于deno
不够成熟,最终放弃。一开始选择deno
的原因是:它原生支持Typescript语法,不必使用额外的库进行转码,最关键的是m3u8转mp4工具本身不需要第三方库支持。
deno
的远程请求需要使用fetch来实现,fetch在请求https资源的时,如果证书出现错误,请求会直接中断,这个问题导致我在请求m3u8文件这一步就卡住了。目前deno
中也没有方法来关闭SSL证书的验证,有人已经给ry提issue了,ry会在deno
0.3的某个版本中添加证书验证的开关。值得一提的是,昨天刚出的v0.3.0版本并没有包含该特性。deno
目前也没有实现node
中的http
和https
请求,所以我也只能切换回node
开发了。
注意:
deno
目前不是很成熟,拿来练手没问题,但用来开发产品还要再等等。
核心逻辑
m3u8文件是视频片段的索引,转换工具的工作就是根据索引下载视频片段,然后合并视频片段。node.js是异步单线程,执行批量下载效率会很高,但如果你用callback回调或者Promise去编码,就会非常痛苦,尤其是关键节点的流程控制,代码会非常繁琐。这里推荐async/await去编码,尽管本质上还是Promise,但可以保证代码简洁易懂。关键节点和代码如下:
解析m3u8主文件
获取m3u8主文件的内容需要通过远程请求来完成,这里我用了axios
库来执行,以此兼容http和https协议。为了避免上文提到的ssl证书验证问题,需要配置httpsAgent,请求代码看上去像下面这样:
const { status, data } = await axios.get(source, {
timeout: 200000,
httpsAgent: new https.Agent({
rejectUnauthorized: false,
agent: false
})
});
获取到m3u8文件内容后,需要做进一步解析,m3u8可能直接包含ts文件片段,也可能包含一个二级的m3u8文件网址,我是根据文件中是否包含#EXT-X-STREAM-INF
来区分的,前者直接下载ts片段,后者需要进一步解析m3u8(递归),这里还要区分文件路径是相对还是绝对的,代码大致如下:
if (data.includes("#EXT-X-STREAM-INF")) {
if(item.startsWith('http')){
resolve(await parseM3u8(item));
}else{
resolve(await parseM3u8(`${sourceUrl}/${item}`));
}
} else {
resolve(
list.map(item => {
if (item.startsWith("http")) {
return item;
} else {
return `${sourceUrl}/${item}`;
}
})
);
}
下载ts视频片段
下载的难点在于我们既要利用好node.js
的异步单线程的性能,又要避免过多的io操作阻塞线程。这里我们把得到的ts文件数组分片成了10个一组的二维数组,所以代码里有一段是用来做数组分片的:
const splitDownloadList = list.reduce((current, next, index) => {
if (index % 10 === 0) {
current.push([next]);
return current;
} else {
current[Math.floor(index / 10)].push(next);
return current;
}
}, []);
接下来,我们就按照一次10个ts文件进行下载,这样恰好能榨干node的性能。这里用到了Promise.all()
,保证10个下载任务都完成后进入下个下载过程。而在for循环体中使用await可以有效阻塞循环,让下载工作有序进行。
let index = 0;
for (let item of splitDownloadList) {
console.log(`downloading ${++index} region..........................`);
await Promise.all(
item.map(async (item, n) => {
return downloadTsItem(item, (index - 1) * 10 + n);
})
);
}
console.log("Downloading finished!");
合成mp4视频文件
下载任务完成后,需要对ts视频片段进行合并,我们使用node中的fs.appendFileSync
来合并视频,这里有个问题是,该函数不能接收流,只能接受Buffer格式的数据。所以我们还需要写个stream转buffer的函数:
const streamToBuffer = stream => {
return new Promise(async (resolve, reject) => {
try {
const bufferArr = [];
stream.on("data", data => {
bufferArr.push(data);
});
stream.on("end", () => {
resolve(Buffer.concat(bufferArr));
});
} catch (e) {
reject(e);
}
});
};
最后批量调用合并方法,合成mp4文件:
let index = 0;
for (let item of list) {
console.log(`combining NO.${++index} file...`);
const data = await streamToBuffer(
fs.createReadStream(`${targetPath}/${item}`)
);
fs.appendFileSync(outputPath, data);
}
小结
m3u8转mp4的思路很简单,难的是编码的流程控制和性能压榨。善用async/await
可以化繁为简,有效地提高开发效率。最后提醒一句,觉得好用,一定要去https://github.com/supervergil/node-m3u8-to-mp4给我star,你的鼓励是我创作的最大动力!
(全文完)