通过nodejs的buffer计算mp4视频时长


最近在弄一个个人OSS存储器,主要是图片和视频,为了提升体验,对于前端需要一些缩略图或者视频封面、时长等信息,图片缩略图比较容易实现。获取视频时长比较容易想到的方法是通过nodejs调用ffmpeg命令去获取,这是肯定行得通的,但是必须安装FFmpeg插件,而如果直接通过nodejs从MP4文件信息解析出时长,就更方便了。

准备

先去学习mp4文件格式解析,网上有很多解释文档。
参考:https://www.cnblogs.com/ztteng/articles/3048152.html
其实就是找到几个关键字,这些关键字之后的信息大小是固定的,一个是ftyp字段,全文档有且只有一个,里面都是文件应用信息,具体是什么,我们不关心。
还有一个字段是moov字段,这个字段同File Type Box一样,有且只有一个,一般情况下包含1个mvhd和若干个trak
mvhd结构
我们现在只要从mvhd中获取两个信息就可以,一个是”time scale”,另一个是”duration”,两个都是4字节,从mvhd之后的第16个字节开始读4个字节就是”time scale”,再读4个字节就是”duration”,duration/time scale = 视频时长的秒数

操作

  1. 我们先定义一个函数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;
}
  1. 使用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。
moov在前面
moov在后面
这样就导致了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。
959M文件

结语

山穷水复疑无路,柳暗花明又一村
每次碰到问题都是这样,总感觉没辙了,查查资料,翻翻帖子,总能找到灵感。
这可能就是代码的魅力吧。


文章作者: 无名小卒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 无名小卒 !
  目录