cerevox
Version:
TypeScript SDK for browser automation and secure command execution in highly available and scalable micro computer environments
231 lines • 7.19 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMp4Duration = getMp4Duration;
exports.checkMp4HasAudio = checkMp4HasAudio;
// mp4-duration.ts
const fs_1 = require("fs");
function readAtom(buf, offset, end) {
if (offset + 8 > end)
return null;
let size = buf.readUInt32BE(offset);
const type = buf.toString('ascii', offset + 4, offset + 8);
let header = 8;
if (size === 1) {
if (offset + 16 > end)
return null;
const high = buf.readUInt32BE(offset + 8);
const low = buf.readUInt32BE(offset + 12);
size = Number((BigInt(high) << BigInt(32)) | BigInt(low));
header = 16;
}
else if (size === 0) {
size = end - offset; // box 一直到父级末尾
}
// 基础合法性
if (size < header || offset + size > end)
return null;
return {
type,
offset,
header,
size,
contentStart: offset + header,
contentEnd: offset + size,
};
}
function* iterateChildren(buf, parent) {
let p = parent.contentStart;
while (p + 8 <= parent.contentEnd) {
const child = readAtom(buf, p, parent.contentEnd);
if (!child)
break;
yield child;
if (child.size <= 0)
break; // 防御
p += child.size;
}
}
function findChild(buf, parent, type) {
const children = Array.from(iterateChildren(buf, parent));
for (const c of children) {
if (c.type === type)
return c;
}
return null;
}
function findChildren(buf, parent, type) {
const out = [];
const children = Array.from(iterateChildren(buf, parent));
for (const c of children) {
if (c.type === type)
out.push(c);
}
return out;
}
function readUInt64BE(buf, offset) {
const hi = buf.readUInt32BE(offset);
const lo = buf.readUInt32BE(offset + 4);
return (BigInt(hi) << BigInt(32)) | BigInt(lo);
}
/** 读取 mvhd 的 timescale/duration(秒) */
function parseMvhdSeconds(buf, mvhd) {
const base = mvhd.contentStart; // FullBox: version(1) + flags(3) 后跟字段
const version = buf.readUInt8(base);
if (version === 0) {
const timescale = buf.readUInt32BE(base + 12);
const duration = buf.readUInt32BE(base + 16);
if (!timescale)
return null;
return duration / timescale;
}
if (version === 1) {
const timescale = buf.readUInt32BE(base + 20);
const duration = Number(readUInt64BE(buf, base + 24));
if (!timescale)
return null;
return duration / timescale;
}
return null;
}
/** 读取 mdhd 的 timescale/duration(秒) */
function parseMdhdSeconds(buf, mdhd) {
const base = mdhd.contentStart; // FullBox
const version = buf.readUInt8(base);
if (version === 0) {
const timescale = buf.readUInt32BE(base + 12);
const duration = buf.readUInt32BE(base + 16);
if (!timescale)
return null;
return duration / timescale;
}
if (version === 1) {
const timescale = buf.readUInt32BE(base + 20);
const duration = Number(readUInt64BE(buf, base + 24));
if (!timescale)
return null;
return duration / timescale;
}
return null;
}
/** 主函数:获取 MP4 时长(秒) */
async function getMp4Duration(filePath) {
const buf = await fs_1.promises.readFile(filePath);
// 根层遍历,找到 moov(可能在头也可能在尾;faststart 会把 moov 前移)
let moov = null;
{
let p = 0;
const end = buf.length;
while (p + 8 <= end) {
const a = readAtom(buf, p, end);
if (!a)
break;
if (a.type === 'moov') {
moov = a;
break;
}
p += a.size;
}
}
if (!moov)
throw new Error('moov box not found');
// 先尝试 mvhd
const mvhd = findChild(buf, moov, 'mvhd');
const best = mvhd ? parseMvhdSeconds(buf, mvhd) : null;
// 轨道兜底:取所有 trak→mdia→mdhd 的最大值
const traks = findChildren(buf, moov, 'trak');
let trackMax = null;
for (const trak of traks) {
const mdia = findChild(buf, trak, 'mdia');
if (!mdia)
continue;
const mdhd = findChild(buf, mdia, 'mdhd');
if (!mdhd)
continue;
const sec = parseMdhdSeconds(buf, mdhd);
if (sec != null) {
trackMax = trackMax == null ? sec : Math.max(trackMax, sec);
}
}
const final = best ?? trackMax;
if (final == null || !isFinite(final)) {
return null;
}
return final;
}
/**
* 检查MP4文件是否包含音频轨道
* @param filePath MP4文件路径
* @returns Promise<boolean> 是否包含音频轨道
*/
async function checkMp4HasAudio(filePath) {
try {
const buf = await fs_1.promises.readFile(filePath);
// 找到 moov box
let moov = null;
{
let p = 0;
const end = buf.length;
while (p + 8 <= end) {
const a = readAtom(buf, p, end);
if (!a)
break;
if (a.type === 'moov') {
moov = a;
break;
}
p += a.size;
}
}
if (!moov)
return false;
// 遍历所有 trak (轨道)
const traks = findChildren(buf, moov, 'trak');
for (const trak of traks) {
// 查找 trak -> mdia -> hdlr
const mdia = findChild(buf, trak, 'mdia');
if (!mdia)
continue;
const hdlr = findChild(buf, mdia, 'hdlr');
if (!hdlr)
continue;
// 解析 hdlr box 来确定轨道类型
const handlerType = parseHandlerType(buf, hdlr);
if (handlerType === 'soun') {
return true; // 找到音频轨道
}
}
return false; // 没有找到音频轨道
}
catch (error) {
console.warn(`Failed to check MP4 audio for ${filePath}:`, error);
return false;
}
}
/**
* 解析 hdlr box 获取处理器类型
* @param buf Buffer
* @param hdlr hdlr atom
* @returns 处理器类型字符串
*/
function parseHandlerType(buf, hdlr) {
try {
const base = hdlr.contentStart;
// hdlr box 结构: version(1) + flags(3) + pre_defined(4) + handler_type(4) + reserved(12) + name(variable)
if (base + 16 > hdlr.contentEnd)
return null;
// handler_type 在偏移 8 处 (version + flags + pre_defined 后)
const handlerType = buf.toString('ascii', base + 8, base + 12);
return handlerType;
}
catch (error) {
return null;
}
}
// 用法示例
// (async () => {
// const s = await getMp4Duration("example.mp4");
// console.log("视频时长(秒):", s);
// const hasAudio = await checkMp4HasAudio("example.mp4");
// console.log("包含音频:", hasAudio);
// })();
//# sourceMappingURL=mp4-duration.js.map