38 lines
2.7 KiB
TypeScript
38 lines
2.7 KiB
TypeScript
import {execFile} from 'node:child_process';
|
|
import {promisify} from 'node:util';
|
|
|
|
export type MediaRect = {x:number;y:number;width:number;height:number};
|
|
export function toVideoPixelRect(rect:MediaRect,width:number,height:number) {
|
|
if (![rect.x,rect.y,rect.width,rect.height,width,height].every(Number.isFinite) || rect.x<0 || rect.y<0 || rect.width<=0 || rect.height<=0 || rect.x+rect.width>1.001 || rect.y+rect.height>1.001) throw new Error('裁剪区域无效');
|
|
const even=(n:number)=>Math.floor(n/2)*2;
|
|
const x=even(rect.x*width), y=even(rect.y*height);
|
|
const w=Math.min(even(rect.width*width),even(width-x)), h=Math.min(even(rect.height*height),even(height-y));
|
|
if(w<2||h<2) throw new Error('裁剪区域太小');
|
|
return {x,y,width:w,height:h};
|
|
}
|
|
const run=promisify(execFile);
|
|
const inputSafety=['-protocol_whitelist','file','-format_whitelist','mov,matroska,webm,avi,mpeg,mpegts,flv,ogg'];
|
|
export async function processVideoFile(input:string,output:string,operation:'crop'|'extract-audio'|'silent-video',rect?:MediaRect) {
|
|
const {stdout}=await run('ffprobe',['-v','error',...inputSafety,'-show_streams','-show_format','-of','json',input],{timeout:15000,maxBuffer:1024*1024});
|
|
const info=JSON.parse(stdout), video=info.streams?.find((s:any)=>s.codec_type==='video');
|
|
if(!video) throw new Error('素材中没有视频画面');
|
|
if (!(Number(video.width)>0 && Number(video.height)>0) || Number(video.width)*Number(video.height)>33554432) throw new Error('视频尺寸不受支持,请使用 8K 以内的视频');
|
|
const duration=Number(info.format?.duration || 0);
|
|
if(!Number.isFinite(duration)||duration<=0||duration>600) throw new Error('本地视频处理支持 10 分钟以内的视频');
|
|
let args:string[];
|
|
let dimensions={width:Number(video.width),height:Number(video.height)};
|
|
if(operation==='extract-audio') {
|
|
if(!info.streams.some((s:any)=>s.codec_type==='audio')) throw new Error('原视频不包含音轨');
|
|
args=['-map','0:a:0','-vn','-c:a','aac','-b:a','192k'];
|
|
} else if(operation==='silent-video') {
|
|
args=['-map','0:v:0','-an','-c:v','copy','-movflags','+faststart'];
|
|
} else {
|
|
if(!rect) throw new Error('请选择裁剪区域');
|
|
const crop=toVideoPixelRect(rect,dimensions.width,dimensions.height);
|
|
dimensions=crop;
|
|
args=['-map','0:v:0','-map','0:a?','-vf',`crop=${crop.width}:${crop.height}:${crop.x}:${crop.y}`,'-c:v','libx264','-preset','veryfast','-crf','20','-pix_fmt','yuv420p','-c:a','aac','-movflags','+faststart'];
|
|
}
|
|
await run('ffmpeg',['-nostdin','-y','-v','error','-threads','2','-filter_threads','1',...inputSafety,'-i',input,...args,'-threads','2',output],{timeout:180000,maxBuffer:1024*1024});
|
|
return {duration,...dimensions};
|
|
}
|