A simple RESTful web service in nodejs (3)

wget.js完成后,只剩下最后一步了:视频文件的转码与合并。ffmpeg的安装就不赘述了,我是根据ffmpeg官网上的教程一步步编译安装的。用yum或者apt-get方式应该也行,就是版本可能没有那么新。

这个进程的主要功能是把flv等格式的视频,转换成标准mp4视频,并且合并成一个文件。一般来说,flv的视频编码和mp4同是h264,而音频编码往往不一致。所以flv转mp4大部分情况下可以直接copy视频流,只需要转换音频流。像B站那种,经常flv连音频流也是aac的,那就更简单了,只换个container就行。

可以借用下面两张图片直观地说明一下:
flv2mp4
flv2mp4_convert

一开始其实想用exec()直接通过linux命令行执行ffmpeg各种命令的,但是不好处理执行结果的返回以及和主进程的状态通信,所以还是用了一个叫node-fluent-ffmpeg的module,它提供了一层API方便调用(但是用下来感觉并不方便…),最重要的是可以从主进程fork()了。

注意fluent-ffmpeg有个问题,即当你没有使用ffmpeg默认的audio encoder,而是使用libfdk_aac这种的话,输出音频质量设定的参数名是不一样的!这样就造成了.audioQuality()方法不起作用,因为那里是写死了用”-aq”的。详情见一则Github的issue
解决方案有两个,一是把fluent-ffmpeg代码中lib/options/audio.js的下面这行

this._currentOutput.audio('-aq', quality);

改成

this._currentOutput.audio('-vbr', quality);

或者不用.audioQuality(),直接用.outputOptions()来指定相应参数。我这里选择前者。

下面开始正题。首先还是从server.js取得参数,这次是已下载的视频文件路径信息(server.js向ffmpeg.js传递了俩参数,id和视频数量)。

var ffmpeg = require('fluent-ffmpeg');
var myArgs = process.argv.slice(2);
var taskId = myArgs[0];
var num = myArgs[1];
var curDir = './tasks/' + taskId;

接着是三板斧:探测编码、转码、合并。
首先定义一个函数,探测视频文件的编码。因为本程序用于同视频文件的不同分块之间的合并,默认所有分块都使用了同样的codecs,因此只检测第一个分块即可:

var codecs = [];
var probe = ffmpeg(curDir + '-0.tmp');
var doProbe = function() {  // detect codecs
	probe.ffprobe(function(err, data) {
		if (err) {
			process.send({error : true});
			process.exit();
		}
		if (data.streams !== undefined) {
			data.streams.forEach(function(streamdata, index){
				codecs.push(streamdata.codec_name);
			});
		}
		if (data.format !== undefined) {
			console.log('detected format: ' + data.format.format_name);
			if (data.format.format_name.indexOf('mp4') === -1) {
				mp4FormatFlag = false;
			}
			bitRate = (Math.ceil(parseInt(data.format.bit_rate, 10) / 1000)) + 'k';
		}
	});
};

ffprobe是ffmpeg软件包中的一个探测视频编码的工具,用法和输出见参考资料。
这里因为不是很确定video部分是在steams[0]还是steams[1],所以直接把所有steams里的codec_name推到一个数组里,后面再处理。顺便也取得了container名称(flv, mp4等),以及视频整体码率。

接着是转码阶段。如果ffprobe检测出视频编码是h264,或者音频编码是aac,则直接传递copy参数给encoder,即直接拷贝视频/音频流,免去了重编码的步骤。一般来说视频网站针对PC客户端比较少直接提供mp4的,所以这里基本还是得把container从flv转成mp4。

var aCodec = 'copy', vCodec = 'copy';
var aacCodec = true, x264Codec = true;
var mp4FormatFlag = true;
var needConvertFlag;
var convertedFlag = false;
var doConversion = function() { // if not in H.264 + AAC format, convert before concatenation
	if (codecs.indexOf('aac') === -1) {
		aCodec = 'libfdk_aac';
		aacCodec = false;
	}
	if (codecs.indexOf('h264') === -1) {
		vCodec = 'libx264';
		x264Codec = false;
	}
	needConvertFlag = (aacCodec === false) || (x264Codec === false) || (mp4FormatFlag === false);

	// do the conversion if necessary
	if (needConvertFlag === true) {
		fileList.forEach(function(entry, index){
			var cmd = ffmpeg(entry);
			cmd.outputOptions('-movflags faststart');

			cmd.videoCodec(vCodec);
			if (x264Codec === false) {
				cmd.videoBitrate(bitRate);    // bit rate approximates to original one
			}

			cmd.audioCodec(aCodec);
			if (aacCodec === false) {
				cmd.audioQuality(5);    // VBR mode 5 for libfdk_aac (96-112kbps per channel)
			}

			cmd.on('error', function(err) {
					process.send({error : true});
					process.exit();
				})
				.on('end', function() {
					convertedFlag = true;
				});
			cmd.save(entry + '.mp4');
		});
	} else {
		convertedFlag = true;   // if no need to convert, set convertedFlag to true immediately
	}
};

这里偷了个小懒:当判断video codec不是h264时,设定的视频转码输出码率是原视频总体码率,而不是原视频的视频部分码率。话说转码总会有点画质损失不是?码率设高点可能效果更能接近原视频。

做完了必要的转码操作,终于到了最后一关:合并。
好事多磨,最后果然还是发现有个坑:用fluent-ffmpeg的mergeToFile()方法无法实现想要的效果,见我和fluent-ffmpeg作者在github上的讨论。简单说来就是mergeToFile()实现的转码方式不支持copy方法。
又仔细研究了下fluent-ffmpeg相关源代码,终于找到了解决方案:

var doMerge = function() {
	var merge = ffmpeg();
	var listFileName = "./tasks/" + taskId + '.txt', fileNames = '';

	// ffmpeg -f concat -i mylist.txt -c copy output
	fileList.forEach(function(entry, index){
		fileNames = fileNames + 'file ' + '\'' + entry.slice(8);
		if (needConvertFlag === true && convertedFlag === true) {
			fileNames = fileNames + '.mp4';
		}
		fileNames = fileNames + '\'\n';
	});

	fs.writeFileSync(listFileName, fileNames);

	merge.input(listFileName);
	merge.inputOptions(['-f concat', '-safe 0']);
	merge.outputOptions('-c copy');
	merge.on('error', function(err) {
			process.send({error : true});
			process.exit();
		})
		.on('end', function() {
			moveFile();
			process.send({complete : true, filePath : resultPath});
			process.exit();
		})
		.save(resultPath);
};

这里使用了fs module的writeFileSync()方法,不能用async版本的,因为后面马上要用这个文件,必须确定已经正常创建。做完合并后,发message给主进程报告最终生成文件的路径。

到这里三板斧都已打造完成,开始舞动起来!然而立刻发现了一个坑爹的问题:怎样按次序执行这三个步骤?也就是说,怎样让这三个异步函数一个接一个执行,而不是将它们改造成阻塞式函数?
这里再次偷了个懒,继续祭出setInterval大杀器(orz)…

// execute functions in order
doProbe();
var checkProbeFlag = setInterval(function() {
	if (bitRate !== undefined) {
		clearInterval(checkProbeFlag);
		doConversion();
	}
}, 1000);
var checkConversionFlag = setInterval(function() {
	if (convertedFlag === true) {
		clearInterval(checkConversionFlag);
		doMerge();
	}
}, 1000);

其实更好的做法也许是使用async module的series()方法,这个是用于顺序执行异步函数的。不过因为这三个函数本身其实是阻塞式的,它们内部调用的ffmpeg()才是非阻塞的,所以那三个函数内调用callback的时机应该是接收到ffmpeg()返回的’end’ event的时候,而不是写在函数的末尾。

.on('end', function() {
    convertedFlag = true;
    callback(null, 'doConversion');
});
async.series([doProbe, doConversion, doMerge],
	function(err, results){
		if (err) {
			console.log('ffmpeg async functions error: ' + err);
			process.exit();
		}
		console.log('ffmpeg process completed with no error');
	}
);

本系列文章到此结束,有什么问题、建议,请移步github issues

References:
[1] https://github.com/fluent-ffmpeg/node-fluent-ffmpeg
[2] https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/496
[3] https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/348
[4] https://github.com/caolan/async
[5] http://justinklemm.com/node-js-async-tutorial/
[6] http://stackoverflow.com/questions/6898779/how-to-write-asynchronous-functions-for-node-js
[7] https://nodejs.org/api/fs.html#fs_fs_writefilesync_file_data_options
[8] https://addpipe.com/blog/flv-to-mp4/
[9] https://trac.ffmpeg.org/wiki/Concatenate
[10] https://trac.ffmpeg.org/wiki/Encode/AAC
[11] https://trac.ffmpeg.org/wiki/Encode/H.264
[12] https://trac.ffmpeg.org/wiki/FFprobeTips
[13] https://trac.ffmpeg.org/wiki/CompilationGuide/Centos

本文是悠然居(wordpress.youran.me)原创文章,如转载必须保留此告示。

本文为悠然居(https://wordpress.youran.me/)的原创文章,转载请注明出处!

Leave a Reply

Your email address will not be published. Required fields are marked *