优化图文页

This commit is contained in:
2026-04-28 17:11:42 +08:00
parent 674c202264
commit 040273586b
69 changed files with 3939 additions and 2302 deletions
+179 -120
View File
@@ -1,80 +1,113 @@
import { execFile } from 'child_process';
import { promises as fs } from 'fs';
import os from 'os';
import path from 'path';
import { promisify } from 'util';
import { execFile } from "child_process";
import { promises as fs } from "fs";
import os from "os";
import path from "path";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
/**
* 使用 ffmpeg 从视频二进制中提取第一帧,返回 JPEG buffer
*/
export async function extractFirstFrame(videoBuffer: Buffer): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
const ffmpegCmd = process.env.FFMPEG_PATH || 'ffmpeg';
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
const outPath = path.join(tmpDir, `${base}.jpg`);
export async function extractFirstFrame(
videoBuffer: Buffer,
): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
const ffmpegCmd = process.env.FFMPEG_PATH || "ffmpeg";
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
const outPath = path.join(tmpDir, `${base}.jpg`);
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
'-hide_banner',
'-loglevel', 'error',
'-ss', '0',
'-i', inPath,
'-frames:v', '1',
'-q:v', '2',
'-f', 'image2',
'-y',
outPath,
];
await execFileAsync(ffmpegCmd, args, { windowsHide: true });
const img = await fs.readFile(outPath);
return { buffer: img, contentType: 'image/jpeg', ext: 'jpg' };
} catch (e: any) {
if (e && (e.code === 'ENOENT' || /not found|is not recognized/i.test(String(e.message)))) {
console.warn('系统未检测到 ffmpeg,可安装并配置 PATH 或设置 FFMPEG_PATH 后启用封面提取。');
return null;
}
throw e;
} finally {
try { await fs.unlink(inPath); } catch { }
try { await fs.unlink(outPath); } catch { }
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
"-hide_banner",
"-loglevel",
"error",
"-ss",
"0",
"-i",
inPath,
"-frames:v",
"1",
"-q:v",
"2",
"-f",
"image2",
"-y",
outPath,
];
await execFileAsync(ffmpegCmd, args, { windowsHide: true });
const img = await fs.readFile(outPath);
return { buffer: img, contentType: "image/jpeg", ext: "jpg" };
} catch (e: any) {
if (
e &&
(e.code === "ENOENT" ||
/not found|is not recognized/i.test(String(e.message)))
) {
console.warn(
"系统未检测到 ffmpeg,可安装并配置 PATH 或设置 FFMPEG_PATH 后启用封面提取。",
);
return null;
}
throw e;
} finally {
try {
await fs.unlink(inPath);
} catch {}
try {
await fs.unlink(outPath);
} catch {}
}
}
/**
* 使用 ffprobe 获取视频时长(毫秒)
*/
export async function getVideoDuration(videoBuffer: Buffer): Promise<number | null> {
const ffprobeCmd = process.env.FFPROBE_PATH || 'ffprobe';
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
export async function getVideoDuration(
videoBuffer: Buffer,
): Promise<number | null> {
const ffprobeCmd = process.env.FFPROBE_PATH || "ffprobe";
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
inPath,
];
const { stdout } = await execFileAsync(ffprobeCmd, args, { windowsHide: true });
const durationSeconds = parseFloat(stdout.trim());
if (isNaN(durationSeconds)) return null;
return Math.round(durationSeconds * 1000); // 转换为毫秒
} catch (e: any) {
if (e && (e.code === 'ENOENT' || /not found|is not recognized/i.test(String(e.message)))) {
console.warn('系统未检测到 ffprobe,可安装并配置 PATH 或设置 FFPROBE_PATH 后启用时长提取。');
return null;
}
console.warn(`获取视频时长失败: ${e?.message || e}`);
return null;
} finally {
try { await fs.unlink(inPath); } catch { }
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
inPath,
];
const { stdout } = await execFileAsync(ffprobeCmd, args, {
windowsHide: true,
});
const durationSeconds = parseFloat(stdout.trim());
if (isNaN(durationSeconds)) return null;
return Math.round(durationSeconds * 1000); // 转换为毫秒
} catch (e: any) {
if (
e &&
(e.code === "ENOENT" ||
/not found|is not recognized/i.test(String(e.message)))
) {
console.warn(
"系统未检测到 ffprobe,可安装并配置 PATH 或设置 FFPROBE_PATH 后启用时长提取。",
);
return null;
}
console.warn(`获取视频时长失败: ${e?.message || e}`);
return null;
} finally {
try {
await fs.unlink(inPath);
} catch {}
}
}
/**
@@ -88,68 +121,94 @@ export async function getVideoDuration(videoBuffer: Buffer): Promise<number | nu
* 若系统未安装 ffmpeg 或未在 PATH 中,返回 null 并给出提示。
*/
export async function extractAudio(
videoBuffer: Buffer,
opts?: { format?: 'mp3' | 'aac' | 'wav'; bitrateKbps?: number }
videoBuffer: Buffer,
opts?: { format?: "mp3" | "aac" | "wav"; bitrateKbps?: number },
): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
const ffmpegCmd = process.env.FFMPEG_PATH || 'ffmpeg';
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
const ffmpegCmd = process.env.FFMPEG_PATH || "ffmpeg";
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
const format = opts?.format ?? 'mp3';
const bitrate = Math.max(32, Math.min(512, opts?.bitrateKbps ?? 192)); // 安全范围 32~512 kbps
const format = opts?.format ?? "mp3";
const bitrate = Math.max(32, Math.min(512, opts?.bitrateKbps ?? 192)); // 安全范围 32~512 kbps
// 根据目标格式设置输出路径、MIME 与编码参数
let outPath = '';
let contentType = '';
let ext = '';
let codecArgs: string[] = [];
// 根据目标格式设置输出路径、MIME 与编码参数
let outPath = "";
let contentType = "";
let ext = "";
let codecArgs: string[] = [];
if (format === 'mp3') {
ext = 'mp3';
contentType = 'audio/mpeg';
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = ['-c:a', 'libmp3lame', '-b:a', `${bitrate}k`];
} else if (format === 'aac') {
// 使用 m4a 容器更通用
ext = 'm4a';
contentType = 'audio/mp4';
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = ['-c:a', 'aac', '-b:a', `${bitrate}k`, '-movflags', '+faststart'];
} else {
// wav
ext = 'wav';
contentType = 'audio/wav';
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = ['-f', 'wav', '-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2'];
if (format === "mp3") {
ext = "mp3";
contentType = "audio/mpeg";
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = ["-c:a", "libmp3lame", "-b:a", `${bitrate}k`];
} else if (format === "aac") {
// 使用 m4a 容器更通用
ext = "m4a";
contentType = "audio/mp4";
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = [
"-c:a",
"aac",
"-b:a",
`${bitrate}k`,
"-movflags",
"+faststart",
];
} else {
// wav
ext = "wav";
contentType = "audio/wav";
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = [
"-f",
"wav",
"-acodec",
"pcm_s16le",
"-ar",
"44100",
"-ac",
"2",
];
}
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
"-hide_banner",
"-loglevel",
"error",
"-i",
inPath,
"-vn", // 丢弃视频流
...codecArgs,
"-y",
outPath,
];
await execFileAsync(ffmpegCmd, args, { windowsHide: true });
const audio = await fs.readFile(outPath);
return { buffer: audio, contentType, ext };
} catch (e: any) {
if (
e &&
(e.code === "ENOENT" ||
/not found|is not recognized/i.test(String(e.message)))
) {
console.warn(
"系统未检测到 ffmpeg,可安装并配置 PATH 或设置 FFMPEG_PATH 后启用音频提取。",
);
return null;
}
// 一些环境可能缺少特定编码器(如 libmp3lame),提示并抛出原始错误
console.warn(`提取音频失败: ${e?.message || e}`);
throw e;
} finally {
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
'-hide_banner',
'-loglevel', 'error',
'-i', inPath,
'-vn', // 丢弃视频流
...codecArgs,
'-y',
outPath,
];
await execFileAsync(ffmpegCmd, args, { windowsHide: true });
const audio = await fs.readFile(outPath);
return { buffer: audio, contentType, ext };
} catch (e: any) {
if (e && (e.code === 'ENOENT' || /not found|is not recognized/i.test(String(e.message)))) {
console.warn('系统未检测到 ffmpeg,可安装并配置 PATH 或设置 FFMPEG_PATH 后启用音频提取。');
return null;
}
// 一些环境可能缺少特定编码器(如 libmp3lame),提示并抛出原始错误
console.warn(`提取音频失败: ${e?.message || e}`);
throw e;
} finally {
try { await fs.unlink(inPath); } catch { }
try {
if (outPath) await fs.unlink(outPath);
} catch { }
}
await fs.unlink(inPath);
} catch {}
try {
if (outPath) await fs.unlink(outPath);
} catch {}
}
}