最近被一首JISOO的FLOWER歌洗脑,但碍于版权原因,只能在B站上看mv视频,盯着尬舞听歌着实有些尴尬,突发奇想,如果能将视频中的音频和视频分开不就能只听音乐,不用看尴尬的舞蹈吗?刚好手机上有不少B站本地的学习视频想导入到电脑上看,可是B站下载的格式.m4s文件,普通播放器根本点不开,有没有什么工具能将B站中的.m4s文件转换成.mp4文件,方便观看呢。下面介绍一个十分强大的开源工具FFmpeg,一款视频解码器,配合nodejs实现听歌自由(仅个人学习开发使用,切勿商用)
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。详情可参考FFmpeg官网。
(1)在github下载地址进入github下载页面,选择要下载的zip包(根据自己的计算机系统选择)并进行解压,我这里选择win64即可。
(2)解压文件至本地,并进入bin目录下,查看是否有ffmpeg.exe、ffmplay.exe和ffprobe.exe文件,若有三个文件,将bin的根目录路径复制下来,并配置到环境变量中去。
(3)验证是否成功,WIN+R输入cmd窗口下输入指令ffmpeg -version查看是否查询到ffmpeg指令生效。
(1)手机下载B站视频
(2)打开手机文件管理,进入Android->data->tv.danmaku.bili->download目录下,将download里面的文件全部拷贝到电脑要运行的脚本文件目录中(这里我们copy到bilibiliFile文件中。
(1)创建目录bilibiliFile_outMP4文件,用来保存转换后的数据
(2)创建mp4File文件,用来保存原始文件(该文件下的后缀名因保持一致)
(3)创建mp3File文件,用来保存转码后的文件
(1)node版本14.0.0以上
(2)需要用到fs模块、util模块、child_process模块和path模块
(1) VScode通过launch.json运行:本地使用Visual Studio Code这个工具生成launch.json来进行本地debug,可以直接添加配置,一共有两个脚本,分别是BilibiliM4s_mp4.js(执行B站m4s视频转码至mp4文件格式)和mp4_mp3.js(mp4转码成mp3格式文件)。
代码如下(示例):
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "BilibiliM4s_mp4",
"skipFiles": [
"/**"
],
"program": "${workspaceFolder}\\BilibiliM4s_mp4.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
},
{
"type": "node",
"request": "launch",
"name": "mp4_mp3",
"skipFiles": [
"/**"
],
"program": "${workspaceFolder}\\mp4_mp3.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
大致讲述一下就是name指的是运行的名字,runtimeExecutable指的是运行指令,下载了node后就可以使用npm来启动项目,runtimeArgs指的是运行的参数,里面的debug在package.json里面的script里面会有debug的key值。
(2)BilibiliM4s_mp4.js
代码如下(示例):
/**
* 该脚本主要做了将bilibiliFile(B站)中的缓存视频M4S文件转化为普通的mp4文件(node+FFmpeg)
*
* 1、FFmpeg是开源的解决视频、音频相互转码的工具
* (1)github地址:https://github.com/FFmpeg/FFmpeg
* (2)安装包:./FFmpeg安装包(该安装包是win系统)
* (3)运行此脚本,请先安装FFmpeg的插件(直接把(2)解压至电脑,再把这个目录配置到环境变量中去),并配置到电脑的环境变量中去
* (4)验证是否安装成功FFmpeg(cmd窗口输入FFmpeg version,出现有数据则安装成功)
* 2、找到手机缓存的B站的视频,路径./Android/data/tv.danmaku.bili/download
* 3、将B站缓存视频download文件下的所有文件放到bilibilifile文件夹下
* 4、调用bilibiliVideoCache2Mp4Wrap方法,输入参数后方可实现
*/
// bilibili中m4s缓存文件转换成mp4文件
// bilibili手机中保存的地址:./Android/data/tv.danmaku.bili/download
const fs = require('fs');
const path = require('path');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
/**
* 创建要转换mp4的文件夹
* @param {*} targetPath
* @returns
*/
const mkdirSync = async (targetPath) => {
try {
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath)
}
} catch (error) {
console.log(error);
}
}
/**
* 利用FFmpeg通过创建子进程去操作,将audio.m4s和video.m4s文件转化为mp4文件
* @param {*} output 输出的文件夹路径
* @param {*} curDirPath 当前递归目录位置(一层一层找entry.json的数据)
* @param {*} isTarget 是否找到了需要转换成mp4的目标文件夹
* @param {*} outputFileName 转换成mp4的视频名(通过entry.json拿到原来视频的名字)
* @param {*} dir 整个视频合集文件夹的名字
*/
const bilibiliVideoCache2Mp4 = async ({output, curDirPath, isTarget, outputFileName, dir}) => {
if (isTarget) {
/*
使用replace去除空格不然ffmpeg会报错(eg:Unable to find a suitable output format for 'XXX')
这里需要注意targetPath和outputFileName不应该有特殊的字符比如【 】,不然ffmpeg也会报错(eg:Unable to find a suitable output format for 'XXX')
*/
let dirName = dir.replace(new RegExp(/[\\\\/:*?\"<>|\s]/,'g'), '');
let targetPath = `${output}\\${dirName}`
await mkdirSync(targetPath)
outputFileName = outputFileName?.replace(new RegExp(/[\\\\/:*?\"<>|\s]/,'g'), '') // 有可能不存在,因为else调用bilibiliVideoCache2Mp4方法的时候没有传outputFileName,所以要outputFileName?判断是否存在
try {
console.log(`---开始将名为《${outputFileName}》的音频、视频文件合并输出成mp4文件---`)
await exec(`ffmpeg -i ${curDirPath}\\video.m4s -i ${curDirPath}\\audio.m4s -codec copy ${targetPath}/${outputFileName}.mp4`)
console.log(`------已完成对《${outputFileName}》文件的合成------`)
} catch (error) {
console.error(`***《${outputFileName}》文件转换失败***`)
console.log(error.stderr);
}
return
}
let files = fs.readdirSync(curDirPath);
files.forEach(function (file) {
let stat = fs.statSync(curDirPath + '/' + file)
// stat.isDirectory()判断路径是否存在
if (stat.isDirectory()) {
// 文件夹名字如果是80的话,说明已经到了目标文件夹audio.m4s和video.m4s就在里面
// 判断是否能读到文件entry.json的文件
let videoInfojson = {};
try {
videoInfojson = JSON.parse(fs.readFileSync(path.join(curDirPath, 'entry.json'), 'utf-8'));
} catch (error) {
console.log(`${curDirPath}目录下未找到entry.json文件,将继续递归继续找子文件`);
}
if (Object.keys(videoInfojson).length > 0) {
// json.page_data.part是这组视频的名字,json.title是单个视频的名字
bilibiliVideoCache2Mp4({
output,
curDirPath: path.join(curDirPath, file),
isTarget: true,
outputFileName: videoInfojson.page_data?.part ? videoInfojson.page_data.part : videoInfojson.time_create_stamp.toString(), // 没有文件名字就去那下载视频的时间戳当名字
dir: videoInfojson.title
})
}
else bilibiliVideoCache2Mp4({
output,
curDirPath: path.join(curDirPath, file)
})
}
});
}
/**
* 执行转换的管道
* @param {*} entry 当前递归目录位置(这里是bilibiliFile文件夹)
* @param {*} output 创建新的文件夹
*/
async function bilibiliVideoCache2Mp4Wrap(entry, output) {
await mkdirSync(output);
bilibiliVideoCache2Mp4({
curDirPath: entry,
output,
})
}
bilibiliVideoCache2Mp4Wrap(path.join(__dirname, 'bilibiliFile'), path.join(__dirname, 'bilibiliFile_outMP4'))
(3)mp4_mp3.js: 这里面输入的初始参数文件格式可以定义为别的模式(mov,mp4,m4a,3gp,3g2,mj2等都可以)。这里简单介绍一下FFmpeg的指令:
-i 传入的mp4路径
-vn保留原视频,不对原始平做处理
-ar采样率(22050, 441000, 48000)
-ac声道数
-b:a比特率(也可以用ab来代替。mp3有96k, 128k, 192k, 256k, 320k)
-f转成的格式(默认也会是mp3这种自动
代码如下(示例):
/**
* 该脚本主要将视频软件切换成无损画质文件(主要看传参),本代码实例依靠FFmpeg+node批量将mp4格式转换成mp3格式
* 1、先将视频文件导入到mp4File文件中
* 2、新建mp3File文件
* 3、将原始文件的路径,原始文件的风格(即文件后缀名,支持mov,mp4,m4a,3gp,3g2,mj2等视频格式),目标保存路径,目标格式四个参数写入到mp4Tomp3方法中去即可
* 4、运行脚本等待输出结果
*/
const fs = require('fs');
const path = require('path');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
/**
*
* @param {*} param
* @param {string} param.originDir 原始文件路径
* @param {string} param.originFormat 原始文件风格(可选mp4等等)
* @param {string} param.targetDir 目标文件路径
* @param {string} param.targetFormat 目标文件风格(可选mp3等等)
*/
const mp4Tomp3 = async ({originDir, originFormat, targetDir, targetFormat}) => {
// 1、新建歌曲异常和歌曲列表清单
const SongListDir = `${targetDir}\\SongListDir.txt`;
const SongListDirError = `${targetDir}\\SongListDirError.txt`
let files = fs.readdirSync(originDir);
let errorNum = 0;
let errorItemName = ''
for (const file of files) {
// 截取后缀名
const fileLastName = file.substring(file.lastIndexOf('.') + 1);
const filefistName = file.substring(0, file.lastIndexOf('.'))
if (fileLastName === originFormat) {
// 简单解释一下:
// -i 传入的mp4路径 -vn保留原视频,不对原始平做处理 -ar采样率(22050, 441000, 48000) -ac声道数 -b:a比特率(也可以用ab来代替。mp3有96k, 128k, 192k, 256k, 320k) -f转成的格式(默认也会是mp3这种自动
// ffmpeg.exe -i xxx.mp4 -vn -ar 44100 -ac 2 -b:a 192k -f mp3 xxx.mp3
try {
console.log(`---开始将名为《${filefistName}》的${fileLastName}文件类型转换为${targetFormat}类型---`);
await exec(`ffmpeg -i ${originDir}\\${filefistName}.${originFormat} -vn -ar 44100 -ac 2 -b:a 192k -f mp3 ${targetDir}\\${filefistName}.${targetFormat}`)
console.log(`------已完成对《${filefistName}》的${fileLastName}文件类型进行转换------`);
fs.writeFileSync(SongListDir, `\n${file}`, { flag: 'a' });
} catch (error) {
errorNum++;
errorItemName = `${errorItemName}《${file}》`
console.error(`***《${filefistName}》转换失败***`)
console.log(error)
fs.writeFileSync(SongListDir, `\n${file}`, { flag: 'a' });
fs.writeFileSync(SongListDirError, `\n报错提示如下:${error}`, { flag: 'a' });
}
}
}
console.log(`!!!共有${errorNum}个异常,异常的名字为${errorItemName}!!!`)
fs.writeFileSync(SongListDir, `\n\n\n!!!共有${errorNum}个异常,异常的名字为${errorItemName}!!!`, { flag: 'a' });
}
mp4Tomp3({
originDir: path.join(__dirname, 'mp4File'),
originFormat: 'mp4',
targetDir: path.join(__dirname, 'mp3File'),
targetFormat: 'mp3'
})
https://github.com/lp970703/node_job/tree/master/ffmpeg_node
以上就是通过FFmpeg+nodejs实现批量视频转音频的功能,FFmpeg的功能远不止如此,更多的学习可以去FFmpeg官网学习,本人还开放了其他关于nodejs实用脚本,感兴趣可以去本人github地址:node_job中学习