Skip to content

Commit 3993122

Browse files
committed
generate two h.264 mp4 instead of prores with alpha to make encoder not die
1 parent 074ee04 commit 3993122

File tree

1 file changed

+131
-60
lines changed
  • large-scale-viz/high_perf_character_render/src/video

1 file changed

+131
-60
lines changed
Lines changed: 131 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,116 @@
11
use anyhow::{Context, Result};
22
use std::io::Write;
3-
use std::path::Path;
3+
use std::path::{Path, PathBuf};
44
use std::process::{Child, ChildStdin, Command, Stdio};
55

66
pub 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

1417
impl 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

123193
impl 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

Comments
 (0)