最近在弄一个个人OSS存储器,主要是图片和视频,为了提升体验,对于前端需要一些缩略图或者视频封面、时长等信息,图片缩略图比较容易实现。获取视频时长比较容易想到的方法是通过nodejs调用ffmpeg命令去获取,这是肯定行得通的,但是必须安装FFmpeg插件,而如果直接通过nodejs从MP4文件信息解析出时长,就更方便了。
准备
先去学习mp4文件格式解析,网上有很多解释文档。
参考:https://www.cnblogs.com/ztteng/articles/3048152.html
其实就是找到几个关键字,这些关键字之后的信息大小是固定的,一个是ftyp
字段,全文档有且只有一个,里面都是文件应用信息,具体是什么,我们不关心。
还有一个字段是moov
字段,这个字段同File Type Box一样,有且只有一个,一般情况下包含1个mvhd
和若干个trak
。
我们现在只要从mvhd
中获取两个信息就可以,一个是”time scale”,另一个是”duration”,两个都是4字节,从mvhd
之后的第16个字节开始读4个字节就是”time scale”,再读4个字节就是”duration”,duration/time scale = 视频时长的秒数
操作
- 我们先定义一个函数getTime,用来在buffer中找到需要的数据,参数就是读出类的buffer
function getTime(buffer){//这一段是借鉴网上的方法
if(buffer.indexOf(Buffer.from('mvhd')) != -1){
var start = buffer.indexOf(Buffer.from('mvhd'));
}else{
return false;
}
const box_size = buffer.readUInt32BE(start);
const box_type = buffer.readUInt32BE(start+4);
const create_time = buffer.readUInt32BE(start+8);
const modi_time = buffer.readUInt32BE(start+12);
const timeScale = buffer.readUInt32BE(start+16);
const duration = buffer.readUInt32BE(start + 20);
const movieLength = Math.floor(duration / timeScale);
// console.log(start,box_size,box_type,create_time,modi_time,duration,timeScale,movieLength);//注释打开,就能看到更多的信息
return movieLength;
}
- 使用fs模块对一个实例MP4文件进行操作,代码如下
fs.open('./yy.mp4', 'r',function(err,fd){
if (err) {throw err;}
const buff = Buffer.alloc(100);
fs.read(fd, buff , 0 ,100 ,0, function(err, bytesRead, buffer) {
if (err) {throw err;}
const time = getTime(buffer);
console.log(time);
})
})
本来以为这样就结束了,没有想到测试另外一个MP4文件时,返回了false
,通过分析文件结构才发现,并不是所有文件都是moov
跟随ftyp
,有的是在文件中间,比如这个yy.mp4。
这样就导致了buffer中读取的100个字节可能根本就包含不了moov
字段,于是理所当然的想法就是把整个文件都放到buffer中,但是理智告诉我这样并不明智,如果一个视频都好几个G,加上并发,那服务器就GG了。所以更合理的方法应该是,将这段代码封装到一个函数中,只要没有找到moov
,递归调用查找函数,就可以不断查下去,知道找到这段信息。
考虑到回调函数的复杂性,重构了一下代码,使用async/await关键字,结构非常清晰
完整代码如下:
const fs = require('fs');
//声明getTime函数,查找关键字,并计算时长
function getTime(buffer){
if(buffer.indexOf(Buffer.from('mvhd')) != -1){
var start = buffer.indexOf(Buffer.from('mvhd'));
}else{
return false;
}
const box_size = buffer.readUInt32BE(start);
const box_type = buffer.readUInt32BE(start+4);
const create_time = buffer.readUInt32BE(start+8);
const modi_time = buffer.readUInt32BE(start+12);
const timeScale = buffer.readUInt32BE(start+16);
const duration = buffer.readUInt32BE(start + 20);
const movieLength = Math.floor(duration / timeScale);
console.log(start,box_size,box_type,create_time,modi_time,duration,timeScale,movieLength);
if(movieLength === 0){
return false;
}
return movieLength;
}
//声明read()函数,打开文件
async function read(){//使用read() 先返回一个promise
return new Promise((resolve,reject)=>{
fs.open('./6.mp4', 'r',(err,fd)=>{
if(err){
reject(err)
}else{
resolve(fd)
}
})
})
}
//声明readfile函数,读数据
async function readfile(fd,buff,start_position){
return new Promise((resolve,reject)=>{
fs.read(fd, buff , 0 ,10000 ,start_position, function(err, bytesRead, buffer) {
if(err){
reject(err)
}else{
resolve(buffer)
}
})
})
}
//声明主函数
var main = async function (){//声明这个函数中有异步操作,read()会先收到一个promise
var start_position = 0
const buff = Buffer.alloc(10000);
var fd = await read();
for(i = 0 ; i<1000000 ;i++){
var buff_data = await readfile(fd,buff,start_position);
const time = getTime(buff_data);
if(time){
console.log(i);
console.log(time);
i = 1000000;
}else{
start_position = start_position + 9900;
}
}
};
main();
每次创建的buffer长度是10000,也就是10KB,而游标移动的长度是9900,这样是为了保证mvhd
字段信息的完整,下一段和前一段是有重合的部分的,不知道这样有没有意义,因为我相信绝大多数的情况只要读取一次buffer就能找到,个别的情况需要多次,再有极少数的情况可能将mvhd
截为两段。
测试
测试了6个视频,均能够准确得出结果,最小的文件2.46M,最大的959M。
结语
山穷水复疑无路,柳暗花明又一村
每次碰到问题都是这样,总感觉没辙了,查查资料,翻翻帖子,总能找到灵感。
这可能就是代码的魅力吧。