| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- "use strict";
- /**
- * 视频生成模块 - FFmpeg 命令封装
- * 实现 Ken Burns 效果、图片+音频合成等功能
- */
- var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
- };
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.generateVideo = generateVideo;
- exports.generateVideoWithBgm = generateVideoWithBgm;
- exports.generateSlideshow = generateSlideshow;
- exports.getVideoInfo = getVideoInfo;
- const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg"));
- const path_1 = __importDefault(require("path"));
- const fs_1 = __importDefault(require("fs"));
- const OUTPUT_DIR = path_1.default.join(process.cwd(), 'public', 'videos');
- const TEMP_DIR = path_1.default.join(process.cwd(), 'temp');
- // 确保目录存在
- function ensureDirectories() {
- if (!fs_1.default.existsSync(OUTPUT_DIR))
- fs_1.default.mkdirSync(OUTPUT_DIR, { recursive: true });
- if (!fs_1.default.existsSync(TEMP_DIR))
- fs_1.default.mkdirSync(TEMP_DIR, { recursive: true });
- }
- /**
- * 合成图片+音频生成最终视频
- */
- async function generateVideo(imagePath, audioPath, outputPath, config) {
- ensureDirectories();
- return new Promise((resolve, reject) => {
- const command = (0, fluent_ffmpeg_1.default)();
- command
- .input(imagePath)
- .inputOptions(['-loop', '1'])
- .input(audioPath);
- // 构建视频滤镜
- const videoFilters = [];
- // Ken Burns 效果
- if (config.kenburns?.enabled) {
- const kb = config.kenburns;
- videoFilters.push({
- filter: 'zoompan',
- options: {
- z: `if(gte(zoom,${kb.minZoom}),min(zoom+0.001,${kb.maxZoom}),${kb.minZoom})`,
- x: 'iw/2-(iw/zoom/2)',
- y: 'ih/2-(ih/zoom/2)',
- d: 1,
- s: `${config.video.width}x${config.video.height}`,
- fps: config.video.fps,
- },
- });
- }
- // 缩放和填充
- videoFilters.push({
- filter: 'scale',
- options: `${config.video.width}:${config.video.height}:force_original_aspect_ratio=decrease`,
- });
- videoFilters.push({
- filter: 'pad',
- options: `${config.video.width}:${config.video.height}:(ow-iw)/2:(oh-ih)/2:color=black`,
- });
- // 字幕
- if (config.subtitle) {
- const sub = config.subtitle;
- const yPos = sub.position === 'top' ? 20 : sub.position === 'center' ? (config.video.height - 40) / 2 : config.video.height - 60;
- videoFilters.push({
- filter: 'drawtext',
- options: {
- text: sub.text,
- fontsize: sub.fontSize || 24,
- fontcolor: sub.fontColor || 'white',
- borderw: 2,
- bordercolor: 'black',
- x: '(w-text_w)/2',
- y: yPos,
- },
- });
- }
- command.videoFilters(videoFilters);
- command
- .outputOptions([`-af volume=${config.audio.volume}`])
- .outputOptions([
- '-c:v libx264',
- '-preset fast',
- '-crf 18',
- '-c:a aac',
- '-b:a 192k',
- '-pix_fmt yuv420p',
- '-shortest',
- ])
- .output(outputPath);
- command.on('progress', (progress) => {
- if (progress.percent)
- console.log(`Processing: ${progress.percent.toFixed(1)}% done`);
- });
- command.on('end', () => {
- fluent_ffmpeg_1.default.ffprobe(outputPath, (err, metadata) => {
- if (err) {
- reject(err);
- return;
- }
- const stats = fs_1.default.statSync(outputPath);
- resolve({
- duration: Math.round(metadata.format.duration || 0),
- fileSize: stats.size,
- });
- });
- });
- command.on('error', reject);
- command.run();
- });
- }
- /**
- * 合成带背景音乐的混音视频
- */
- async function generateVideoWithBgm(imagePath, audioPath, bgmPath, outputPath, config) {
- ensureDirectories();
- return new Promise((resolve, reject) => {
- const command = (0, fluent_ffmpeg_1.default)();
- command
- .input(imagePath)
- .inputOptions(['-loop 1'])
- .input(audioPath)
- .input(bgmPath)
- .inputOptions(['-stream_loop -1']);
- const videoFilters = [];
- if (config.kenburns?.enabled) {
- const kb = config.kenburns;
- videoFilters.push({
- filter: 'zoompan',
- options: {
- z: `if(gte(zoom,${kb.minZoom}),min(zoom+0.001,${kb.maxZoom}),${kb.minZoom})`,
- x: 'iw/2-(iw/zoom/2)',
- y: 'ih/2-(ih/zoom/2)',
- d: 1,
- s: `${config.video.width}x${config.video.height}`,
- fps: config.video.fps,
- },
- });
- }
- videoFilters.push({
- filter: 'scale',
- options: `${config.video.width}:${config.video.height}:force_original_aspect_ratio=decrease`,
- });
- videoFilters.push({
- filter: 'pad',
- options: `${config.video.width}:${config.video.height}:(ow-iw)/2:(oh-ih)/2:color=black`,
- });
- command.videoFilters(videoFilters);
- // 音频混音
- const bgm = config.bgm;
- command.audioFilters(`[1:a]volume=${config.audio.volume}[main];[2:a]volume=${bgm.volume}[bgm];[main][bgm]amix=inputs=2:duration=longest[aout]`);
- command
- .outputOptions([
- '-tune stillimage',
- '-c:v libx264',
- '-preset fast',
- '-crf 18',
- '-c:a aac',
- '-b:a 192k',
- '-pix_fmt yuv420p',
- '-shortest',
- '-map 0:v',
- '-map [aout]',
- ])
- .output(outputPath);
- command.on('end', () => {
- fluent_ffmpeg_1.default.ffprobe(outputPath, (err, metadata) => {
- if (err) {
- reject(err);
- return;
- }
- const stats = fs_1.default.statSync(outputPath);
- resolve({ duration: Math.round(metadata.format.duration || 0), fileSize: stats.size });
- });
- });
- command.on('error', reject);
- command.run();
- });
- }
- /**
- * 生成多图轮播视频
- */
- async function generateSlideshow(images, audioPath, outputPath, config) {
- ensureDirectories();
- const concatListPath = path_1.default.join(TEMP_DIR, `concat_${Date.now()}.txt`);
- const concatList = images.map((img) => `file '${img.path}'`).join('\n');
- fs_1.default.writeFileSync(concatListPath, concatList);
- return new Promise((resolve, reject) => {
- const command = (0, fluent_ffmpeg_1.default)();
- command
- .input(concatListPath)
- .inputOptions(['-f concat', '-safe 0'])
- .input(audioPath);
- const videoFilters = [];
- videoFilters.push({
- filter: 'scale',
- options: `${config.video.width}:${config.video.height}:force_original_aspect_ratio=decrease`,
- });
- videoFilters.push({
- filter: 'pad',
- options: `${config.video.width}:${config.video.height}:(ow-iw)/2:(oh-ih)/2:color=black`,
- });
- if (config.subtitle) {
- const sub = config.subtitle;
- const yPos = sub.position === 'top' ? 20 : sub.position === 'center' ? (config.video.height - 40) / 2 : config.video.height - 60;
- videoFilters.push({
- filter: 'drawtext',
- options: {
- text: sub.text,
- fontsize: sub.fontSize || 24,
- fontcolor: sub.fontColor || 'white',
- borderw: 2,
- bordercolor: 'black',
- x: '(w-text_w)/2',
- y: yPos,
- },
- });
- }
- command.videoFilters(videoFilters);
- command
- .outputOptions([
- '-c:v libx264',
- '-preset fast',
- '-crf 18',
- '-c:a aac',
- '-b:a 192k',
- '-pix_fmt yuv420p',
- '-shortest',
- ])
- .output(outputPath);
- command.on('end', () => {
- fs_1.default.unlinkSync(concatListPath);
- fluent_ffmpeg_1.default.ffprobe(outputPath, (err, metadata) => {
- if (err) {
- reject(err);
- return;
- }
- const stats = fs_1.default.statSync(outputPath);
- resolve({ duration: Math.round(metadata.format.duration || 0), fileSize: stats.size });
- });
- });
- command.on('error', (err) => {
- fs_1.default.unlinkSync(concatListPath);
- reject(err);
- });
- command.run();
- });
- }
- /**
- * 获取视频信息
- */
- async function getVideoInfo(filePath) {
- return new Promise((resolve, reject) => {
- fluent_ffmpeg_1.default.ffprobe(filePath, (err, metadata) => {
- if (err) {
- reject(err);
- return;
- }
- const videoStream = metadata.streams.find((s) => s.codec_type === 'video');
- const stats = fs_1.default.statSync(filePath);
- resolve({
- duration: Math.round(metadata.format.duration || 0),
- width: videoStream?.width || 0,
- height: videoStream?.height || 0,
- size: stats.size,
- });
- });
- });
- }
|