11use anyhow:: { Context , Result } ;
22use std:: io:: Write ;
3- use std:: path:: Path ;
3+ use std:: path:: { Path , PathBuf } ;
44use std:: process:: { Child , ChildStdin , Command , Stdio } ;
55
66pub struct ProResEncoder {
7- ffmpeg_process : Child ,
8- stdin : Option < ChildStdin > ,
7+ rgb_process : Child ,
8+ mask_process : Child ,
9+ rgb_stdin : Option < ChildStdin > ,
10+ mask_stdin : Option < ChildStdin > ,
911 width : u32 ,
1012 height : u32 ,
11- fps : u32 ,
13+ rgb_buffer : Vec < u8 > ,
14+ mask_buffer : Vec < u8 > ,
1215}
1316
1417impl ProResEncoder {
15- /// Create a new ProRes 4444 encoder that streams to a file
18+ /// Create a new dual H.264 encoder (RGB + mask) that streams to two files
1619 pub fn new < P : AsRef < Path > > (
1720 output_path : P ,
1821 width : u32 ,
1922 height : u32 ,
2023 fps : u32 ,
2124 ) -> Result < Self > {
25+ let output_path = output_path. as_ref ( ) ;
26+
27+ // Create two output paths: _rgb.mp4 and _mask.mp4
28+ let mut rgb_path = PathBuf :: from ( output_path) ;
29+ let mut mask_path = PathBuf :: from ( output_path) ;
30+
31+ let stem = output_path. file_stem ( ) . unwrap ( ) . to_str ( ) . unwrap ( ) ;
32+ rgb_path. set_file_name ( format ! ( "{}_rgb.mp4" , stem) ) ;
33+ mask_path. set_file_name ( format ! ( "{}_mask.mp4" , stem) ) ;
34+
2235 log:: info!(
23- "Starting FFmpeg encoder: {}x{} @ {} fps -> {:?}" ,
24- width,
25- height,
26- fps,
27- output_path. as_ref( )
36+ "Starting dual H.264 encoders: {}x{} @ {} fps" ,
37+ width, height, fps
2838 ) ;
39+ log:: info!( " RGB output: {:?}" , rgb_path) ;
40+ log:: info!( " Mask output: {:?}" , mask_path) ;
41+
42+ // Build RGB encoder (H.264, high quality)
43+ let mut rgb_process = Command :: new ( "ffmpeg" )
44+ . args ( & [
45+ "-y" ,
46+ "-f" , "rawvideo" ,
47+ "-pixel_format" , "rgb24" ,
48+ "-video_size" , & format ! ( "{}x{}" , width, height) ,
49+ "-framerate" , & format ! ( "{}" , fps) ,
50+ "-i" , "pipe:0" ,
51+ "-c:v" , "libx264" ,
52+ "-preset" , "slow" ,
53+ "-crf" , "15" , // Near-lossless quality (comparable to ProRes 4444)
54+ "-pix_fmt" , "yuv444p" , // 4:4:4 chroma subsampling for max quality
55+ "-threads" , "8" ,
56+ rgb_path. to_str ( ) . unwrap ( ) ,
57+ ] )
58+ . stdin ( Stdio :: piped ( ) )
59+ . stdout ( Stdio :: null ( ) )
60+ . stderr ( Stdio :: inherit ( ) )
61+ . spawn ( )
62+ . context ( "Failed to spawn RGB encoder" ) ?;
2963
30- // Build ffmpeg command
31- // Input: raw RGBA frames from stdin
32- // Output: ProRes 4444 with alpha channel
33- let mut ffmpeg_process = Command :: new ( "ffmpeg" )
64+ // Build mask encoder (H.264, grayscale)
65+ let mut mask_process = Command :: new ( "ffmpeg" )
3466 . args ( & [
35- "-y" , // Overwrite output file
36- "-f" ,
37- "rawvideo" ,
38- "-pixel_format" ,
39- "rgba" ,
40- "-video_size" ,
41- & format ! ( "{}x{}" , width, height) ,
42- "-framerate" ,
43- & format ! ( "{}" , fps) ,
44- "-i" ,
45- "pipe:0" , // Read from stdin
46- "-threads" ,
47- "8" , // Use 8 threads for encoding
48- "-max_muxing_queue_size" ,
49- "16" , // Small muxing queue to limit buffering
50- "-c:v" ,
51- "prores_ks" , // ProRes encoder
52- "-profile:v" ,
53- "4444" , // ProRes 4444 with alpha
54- "-pix_fmt" ,
55- "yuva444p10le" , // Pixel format with alpha
56- "-vendor" ,
57- "apl0" , // Apple vendor ID
58- output_path. as_ref ( ) . to_str ( ) . unwrap ( ) ,
67+ "-y" ,
68+ "-f" , "rawvideo" ,
69+ "-pixel_format" , "gray" ,
70+ "-video_size" , & format ! ( "{}x{}" , width, height) ,
71+ "-framerate" , & format ! ( "{}" , fps) ,
72+ "-i" , "pipe:0" ,
73+ "-c:v" , "libx264" ,
74+ "-preset" , "slow" ,
75+ "-crf" , "15" ,
76+ "-pix_fmt" , "yuv420p" ,
77+ "-threads" , "8" ,
78+ mask_path. to_str ( ) . unwrap ( ) ,
5979 ] )
6080 . stdin ( Stdio :: piped ( ) )
6181 . stdout ( Stdio :: null ( ) )
6282 . stderr ( Stdio :: inherit ( ) )
6383 . spawn ( )
64- . context ( "Failed to spawn ffmpeg process " ) ?;
84+ . context ( "Failed to spawn mask encoder " ) ?;
6585
66- let stdin = ffmpeg_process
86+ let rgb_stdin = rgb_process
6787 . stdin
6888 . take ( )
69- . context ( "Failed to open ffmpeg stdin" ) ?;
89+ . context ( "Failed to open RGB encoder stdin" ) ?;
90+
91+ let mask_stdin = mask_process
92+ . stdin
93+ . take ( )
94+ . context ( "Failed to open mask encoder stdin" ) ?;
95+
96+ let pixel_count = ( width * height) as usize ;
97+ let rgb_buffer = vec ! [ 0u8 ; pixel_count * 3 ] ;
98+ let mask_buffer = vec ! [ 0u8 ; pixel_count] ;
7099
71100 Ok ( Self {
72- ffmpeg_process,
73- stdin : Some ( stdin) ,
101+ rgb_process,
102+ mask_process,
103+ rgb_stdin : Some ( rgb_stdin) ,
104+ mask_stdin : Some ( mask_stdin) ,
74105 width,
75106 height,
76- fps,
107+ rgb_buffer,
108+ mask_buffer,
77109 } )
78110 }
79111
80112 /// Write a single frame (RGBA8, row-major, top-to-bottom)
113+ /// Splits into RGB and alpha mask streams
81114 pub fn write_frame ( & mut self , frame_data : & [ u8 ] ) -> Result < ( ) > {
82115 let expected_size = ( self . width * self . height * 4 ) as usize ;
83116 if frame_data. len ( ) != expected_size {
@@ -88,41 +121,79 @@ impl ProResEncoder {
88121 ) ;
89122 }
90123
91- if let Some ( stdin) = & mut self . stdin {
124+ // Split RGBA into RGB and alpha channels
125+ let pixel_count = ( self . width * self . height ) as usize ;
126+
127+ for i in 0 ..pixel_count {
128+ let rgba_idx = i * 4 ;
129+ let rgb_idx = i * 3 ;
130+
131+ // RGB channels
132+ self . rgb_buffer [ rgb_idx] = frame_data[ rgba_idx] ;
133+ self . rgb_buffer [ rgb_idx + 1 ] = frame_data[ rgba_idx + 1 ] ;
134+ self . rgb_buffer [ rgb_idx + 2 ] = frame_data[ rgba_idx + 2 ] ;
135+
136+ // Alpha channel
137+ self . mask_buffer [ i] = frame_data[ rgba_idx + 3 ] ;
138+ }
139+
140+ // Write RGB frame
141+ if let Some ( stdin) = & mut self . rgb_stdin {
92142 stdin
93- . write_all ( frame_data)
94- . context ( "Failed to write frame to ffmpeg" ) ?;
95- Ok ( ( ) )
143+ . write_all ( & self . rgb_buffer )
144+ . context ( "Failed to write RGB frame to ffmpeg" ) ?;
96145 } else {
97- anyhow:: bail!( "Encoder stdin is closed" )
146+ anyhow:: bail!( "RGB encoder stdin is closed" )
98147 }
148+
149+ // Write mask frame
150+ if let Some ( stdin) = & mut self . mask_stdin {
151+ stdin
152+ . write_all ( & self . mask_buffer )
153+ . context ( "Failed to write mask frame to ffmpeg" ) ?;
154+ } else {
155+ anyhow:: bail!( "Mask encoder stdin is closed" )
156+ }
157+
158+ Ok ( ( ) )
99159 }
100160
101- /// Finish encoding and close the file
161+ /// Finish encoding and close both files
102162 pub fn finish ( mut self ) -> Result < ( ) > {
103163 log:: info!( "Finalizing video encoding..." ) ;
104164
105165 // Close stdin to signal end of input
106- drop ( self . stdin . take ( ) ) ;
166+ drop ( self . rgb_stdin . take ( ) ) ;
167+ drop ( self . mask_stdin . take ( ) ) ;
107168
108- // Wait for ffmpeg to finish
109- let status = self
110- . ffmpeg_process
169+ // Wait for both encoders to finish
170+ let rgb_status = self
171+ . rgb_process
111172 . wait ( )
112- . context ( "Failed to wait for ffmpeg process " ) ?;
173+ . context ( "Failed to wait for RGB encoder " ) ?;
113174
114- if status. success ( ) {
115- log:: info!( "Video encoding completed successfully" ) ;
175+ let mask_status = self
176+ . mask_process
177+ . wait ( )
178+ . context ( "Failed to wait for mask encoder" ) ?;
179+
180+ if rgb_status. success ( ) && mask_status. success ( ) {
181+ log:: info!( "Both video encodings completed successfully" ) ;
116182 Ok ( ( ) )
117183 } else {
118- anyhow:: bail!( "FFmpeg exited with error: {:?}" , status) ;
184+ anyhow:: bail!(
185+ "FFmpeg exited with errors - RGB: {:?}, Mask: {:?}" ,
186+ rgb_status,
187+ mask_status
188+ ) ;
119189 }
120190 }
121191}
122192
123193impl Drop for ProResEncoder {
124194 fn drop ( & mut self ) {
125- // Try to terminate ffmpeg if it's still running
126- let _ = self . ffmpeg_process . kill ( ) ;
195+ // Try to terminate both encoders if they're still running
196+ let _ = self . rgb_process . kill ( ) ;
197+ let _ = self . mask_process . kill ( ) ;
127198 }
128199}
0 commit comments