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会在deno0.3的某个版本中添加证书验证的开关。值得一提的是,昨天刚出的v0.3.0版本并没有包含该特性。deno目前也没有实现node中的httphttps请求,所以我也只能切换回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,你的鼓励是我创作的最大动力!

(全文完)

0 条评论
回复
支持 Markdown 语法
暂无评论,来抢个沙发吧!