11import { ffprobeGetMediaDuration , ffprobeVideoInfo } from "./ffprobe" ;
22
3+ let hardwareEncodersCache : { [ key : string ] : boolean } | null = null ;
4+
5+ const detectHardwareEncoders = async ( ) : Promise < { [ key : string ] : boolean } > => {
6+ if ( hardwareEncodersCache ) return hardwareEncodersCache ;
7+
8+ const platform = $mapi . app . platformName ( ) ;
9+ const encoders : { [ key : string ] : boolean } = { } ;
10+
11+ try {
12+ const output = await new Promise < string > ( ( resolve , reject ) => {
13+ let buffer = "" ;
14+ $mapi . app . spawnBinary ( "ffmpeg" , [ "-encoders" ] , {
15+ shell : false ,
16+ stdout : ( data : string ) => buffer += data ,
17+ stderr : ( data : string ) => buffer += data ,
18+ success : ( ) => resolve ( buffer ) ,
19+ error : ( msg : string ) => reject ( msg ) ,
20+ } ) ;
21+ } ) ;
22+ // Check for common hardware encoders based on platform
23+ if ( platform === "osx" ) {
24+ // macOS VideoToolbox
25+ if ( output . includes ( "h264_videotoolbox" ) ) encoders . h264_videotoolbox = true ;
26+ if ( output . includes ( "hevc_videotoolbox" ) ) encoders . hevc_videotoolbox = true ;
27+ } else if ( platform === "win" ) {
28+ // Windows
29+ if ( output . includes ( "h264_nvenc" ) ) encoders . h264_nvenc = true ;
30+ if ( output . includes ( "hevc_nvenc" ) ) encoders . hevc_nvenc = true ;
31+ if ( output . includes ( "h264_amf" ) ) encoders . h264_amf = true ;
32+ if ( output . includes ( "hevc_amf" ) ) encoders . hevc_amf = true ;
33+ if ( output . includes ( "h264_qsv" ) ) encoders . h264_qsv = true ;
34+ if ( output . includes ( "hevc_qsv" ) ) encoders . hevc_qsv = true ;
35+ } else if ( platform === "linux" ) {
36+ // Linux
37+ if ( output . includes ( "h264_nvenc" ) ) encoders . h264_nvenc = true ;
38+ if ( output . includes ( "hevc_nvenc" ) ) encoders . hevc_nvenc = true ;
39+ if ( output . includes ( "h264_amf" ) ) encoders . h264_amf = true ;
40+ if ( output . includes ( "hevc_amf" ) ) encoders . hevc_amf = true ;
41+ if ( output . includes ( "h264_qsv" ) ) encoders . h264_qsv = true ;
42+ if ( output . includes ( "hevc_qsv" ) ) encoders . hevc_qsv = true ;
43+ if ( output . includes ( "h264_vaapi" ) ) encoders . h264_vaapi = true ;
44+ if ( output . includes ( "hevc_vaapi" ) ) encoders . hevc_vaapi = true ;
45+ }
46+ } catch ( e ) {
47+ // If detection fails, assume no hardware acceleration
48+ }
49+ hardwareEncodersCache = encoders ;
50+ return encoders ;
51+ } ;
52+
53+ const optimizeArgs = ( args : string [ ] , encoders : { [ key : string ] : boolean } ) : string [ ] => {
54+ const optimizedArgs = [ ...args ] ;
55+ // Replace software encoders with hardware ones if available
56+ for ( let i = 0 ; i < optimizedArgs . length ; i ++ ) {
57+ if ( optimizedArgs [ i ] === "-c:v" ) {
58+ const encoder = optimizedArgs [ i + 1 ] ;
59+ if ( encoder === "libx264" && encoders . h264_nvenc ) {
60+ optimizedArgs [ i + 1 ] = "h264_nvenc" ;
61+ } else if ( encoder === "libx264" && encoders . h264_amf ) {
62+ optimizedArgs [ i + 1 ] = "h264_amf" ;
63+ } else if ( encoder === "libx264" && encoders . h264_qsv ) {
64+ optimizedArgs [ i + 1 ] = "h264_qsv" ;
65+ } else if ( encoder === "libx264" && encoders . h264_videotoolbox ) {
66+ optimizedArgs [ i + 1 ] = "h264_videotoolbox" ;
67+ } else if ( encoder === "libx265" && encoders . hevc_nvenc ) {
68+ optimizedArgs [ i + 1 ] = "hevc_nvenc" ;
69+ } else if ( encoder === "libx265" && encoders . hevc_amf ) {
70+ optimizedArgs [ i + 1 ] = "hevc_amf" ;
71+ } else if ( encoder === "libx265" && encoders . hevc_qsv ) {
72+ optimizedArgs [ i + 1 ] = "hevc_qsv" ;
73+ } else if ( encoder === "libx265" && encoders . hevc_videotoolbox ) {
74+ optimizedArgs [ i + 1 ] = "hevc_videotoolbox" ;
75+ }
76+ }
77+ }
78+
79+ return optimizedArgs ;
80+ } ;
81+
82+ const extractInputFile = ( args : string [ ] ) : string | null => {
83+ for ( let i = 0 ; i < args . length ; i ++ ) {
84+ if ( args [ i ] === "-i" && i + 1 < args . length ) {
85+ return args [ i + 1 ] ;
86+ }
87+ }
88+ return null ;
89+ } ;
90+
91+ export const ffmpegOptimized = async (
92+ args : string [ ] ,
93+ option ?: {
94+ successFileCheck ?: string ,
95+ onProgress ?: ( progress : number ) => void ;
96+ codesOptimized ?: boolean ,
97+ }
98+ ) : Promise < void > => {
99+
100+ option = Object . assign ( {
101+ successFileCheck : '' ,
102+ codesOptimized : false ,
103+ onProgress : undefined ,
104+ } , option )
105+
106+ // add hide banner and loglevel error
107+ if ( ! args . includes ( "-hide_banner" ) ) {
108+ args . unshift ( "-hide_banner" ) ;
109+ }
110+ // if (!args.includes("-loglevel")) {
111+ // args.unshift("-loglevel", "info");
112+ // }
113+
114+ let optimizedArgs = args ;
115+ if ( option ! . codesOptimized ) {
116+ const encoders = await detectHardwareEncoders ( ) ;
117+ const optimizedArgs = optimizeArgs ( args , encoders ) ;
118+ if ( optimizedArgs . join ( ' ' ) !== args . join ( ' ' ) ) {
119+ $mapi . log . info ( 'FfmpegCommandOptimized' , {
120+ original : 'ffmpeg ' + args . join ( ' ' ) ,
121+ optimized : 'ffmpeg ' + optimizedArgs . join ( ' ' )
122+ } ) ;
123+ }
124+ }
125+
126+ let totalDuration = 0 ;
127+ const inputFile = extractInputFile ( optimizedArgs ) ;
128+ if ( inputFile && option ?. onProgress ) {
129+ try {
130+ totalDuration = await ffprobeGetMediaDuration ( inputFile , false ) ;
131+ } catch ( e ) {
132+ // If can't get duration, progress won't work
133+ }
134+ }
135+
136+ return new Promise < void > ( ( resolve , reject ) => {
137+ let lastProgress = 0 ;
138+ const controller = $mapi . app . spawnBinary ( "ffmpeg" , optimizedArgs , {
139+ shell : false ,
140+ stdout : ( data : string ) => {
141+ // console.log("FFmpeg stdout:", data);
142+ } ,
143+ stderr : ( data : string ) => {
144+ // console.log("FFmpeg stderr:", data);
145+ if ( option ?. onProgress && totalDuration > 0 ) {
146+ const timeMatch = data . match ( / t i m e = ( \d + ) : ( \d + ) : ( \d + \. \d + ) / ) ;
147+ if ( timeMatch ) {
148+ const hours = parseInt ( timeMatch [ 1 ] ) ;
149+ const minutes = parseInt ( timeMatch [ 2 ] ) ;
150+ const seconds = parseFloat ( timeMatch [ 3 ] ) ;
151+ const currentTime = hours * 3600 + minutes * 60 + seconds ;
152+ const progress = Math . min ( currentTime / totalDuration , 1 ) ;
153+ if ( progress > lastProgress ) {
154+ option . onProgress ( progress ) ;
155+ lastProgress = progress ;
156+ }
157+ }
158+ }
159+ } ,
160+ success : ( ) => {
161+ if ( option ?. onProgress ) {
162+ option . onProgress ( 1 ) ;
163+ }
164+ if ( option ?. successFileCheck ) {
165+ $mapi . file . exists ( option . successFileCheck ) . then ( exists => {
166+ if ( exists ) {
167+ resolve ( ) ;
168+ } else {
169+ reject ( `FFmpeg completed but output file not found: ${ option ?. successFileCheck } ` ) ;
170+ }
171+ } ) . catch ( reject ) ;
172+ } else {
173+ resolve ( ) ;
174+ }
175+ } ,
176+ error : ( msg : string , exitCode : number ) => {
177+ reject ( `FFmpeg error (code ${ exitCode } ): ${ msg } ` ) ;
178+ } ,
179+ } ) ;
180+ } ) ;
181+ } ;
182+
3183export const ffmpegSetMediaRatio = async (
4184 input : string ,
5185 output : string ,
@@ -45,7 +225,7 @@ export const ffmpegSetMediaRatio = async (
45225 "-map" ,
46226 "[a]" ,
47227 "-preset" ,
48- "fast " ,
228+ "ultrafast " ,
49229 "-y" ,
50230 output ,
51231 ] ;
@@ -238,14 +418,12 @@ export const ffmpegCombineVideoAudio = async (video: string, audio: string) => {
238418 video ,
239419 "-i" ,
240420 audio ,
241- "-c:v" ,
242- "copy" ,
243- "-c:a" ,
244- "aac" ,
245- "-map" ,
246- "0:v:0" ,
247- "-map" ,
248- "1:a:0" ,
421+ "-c:v" , "libx264" ,
422+ "-preset" , "ultrafast" ,
423+ "-crf" , "0" ,
424+ "-c:a" , "aac" ,
425+ "-map" , "0:v:0" ,
426+ "-map" , "1:a:0" ,
249427 "-y" ,
250428 output ,
251429 ] ) ;
@@ -360,7 +538,7 @@ export const ffmpegVideoNormal = async (input: string, option: {
360538 "-r" ,
361539 targetFps . toString ( ) ,
362540 "-preset" ,
363- "fast " ,
541+ "ultrafast " ,
364542 "-y" ,
365543 output ,
366544 ] ;
@@ -372,6 +550,55 @@ export const ffmpegVideoNormal = async (input: string, option: {
372550 return output ;
373551}
374552
553+ export async function ffmpegCutVideo ( input : string , startMs : number , endMs : number ) : Promise < string > {
554+ const output = await $mapi . file . temp ( 'mp4' ) ;
555+ const startSeconds = startMs / 1000 ;
556+ const durationSeconds = ( endMs - startMs ) / 1000 ;
557+ const args = [
558+ '-i' , input ,
559+ '-ss' , startSeconds . toString ( ) ,
560+ '-t' , durationSeconds . toString ( ) ,
561+ '-c:v' , 'libx264' ,
562+ '-preset' , 'ultrafast' ,
563+ '-crf' , '0' ,
564+ '-c:a' , 'aac' ,
565+ '-avoid_negative_ts' , 'make_zero' ,
566+ '-y' , output
567+ ] ;
568+ await ffmpegOptimized ( args , {
569+ successFileCheck : output
570+ } ) ;
571+ return output ;
572+ }
573+
574+ // FFmpeg 工具函数:合并多个视频
575+ export async function ffmpegConcatVideos ( videos : string [ ] ) : Promise < string > {
576+ if ( videos . length === 0 ) {
577+ throw new Error ( 'No videos to concat' ) ;
578+ }
579+ if ( videos . length === 1 ) {
580+ return videos [ 0 ] ;
581+ }
582+ const output = await $mapi . file . temp ( 'mp4' ) ;
583+ const txtFile = await $mapi . file . temp ( 'txt' ) ;
584+ // 创建 concat 文件列表
585+ const lines = videos . map ( video => `file '${ video . replace ( / ' / g, "'\\''" ) } '` ) ;
586+ await $mapi . file . write ( txtFile , lines . join ( '\n' ) ) ;
587+ const args = [
588+ '-f' , 'concat' ,
589+ '-safe' , '0' ,
590+ '-i' , txtFile ,
591+ '-c:v' , 'libx264' ,
592+ '-preset' , 'ultrafast' ,
593+ '-crf' , '0' ,
594+ '-c:a' , 'aac' ,
595+ '-y' , output
596+ ] ;
597+ await ffmpegOptimized ( args , {
598+ successFileCheck : output
599+ } ) ;
600+ return output ;
601+ }
375602
376603export const ffmpegVideoNormal = async ( input : string , option : {
377604 widthMax ?: number ;
0 commit comments