Skip to content

Commit ebcb433

Browse files
committed
[+] feat: use ffmpeg with libx264 video encoder for Windows
1 parent 0718512 commit ebcb433

File tree

8 files changed

+300
-7
lines changed

8 files changed

+300
-7
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,5 @@ The recorder library provides the core screen recording functionality:
103103
2. For GUI changes: `make slint-viewer-desktop` for live preview
104104
3. For integration testing: `make test` to run all workspace tests
105105
4. Examples are in `lib/recorder/examples/` for testing specific functionality
106-
- 不要使用模拟数据
106+
- 不要使用模拟数据
107+
- 不要使用 plachoder 或模拟数据

Cargo.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ console_log = "1.0"
8282
crypto-hash = "0.3"
8383
platform-dirs = "0.3"
8484

85-
8685
wio = "0.2"
8786
yuv = "0.8"
8887
mp4 = "0.14"
@@ -106,6 +105,7 @@ thiserror = "2.0"
106105
crossbeam = "0.8"
107106
spin_sleep = "1.3"
108107
nnnoiseless = "0.5"
108+
ffmpeg-next = "8.0"
109109
derive_builder = "0.20"
110110
wayland-client = "0.31"
111111
fast_image_resize = "5.3"

flake.nix

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828
alsa-lib.dev
2929
pipewire.dev
3030
x264.dev
31+
ffmpeg.dev
3132
];
3233

3334
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
34-
targets = [ "aarch64-linux-android" "wasm32-unknown-unknown" ];
35+
# targets = [ "aarch64-linux-android" "wasm32-unknown-unknown" ];
3536
};
3637
in {
3738
devShells.${system}.default =

lib/recorder/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ derive_setters.workspace = true
2626
screen-capture.workspace = true
2727
openh264 = { workspace = true, optional = true }
2828
yuv = { workspace = true, features = ["rayon"] }
29+
ffmpeg-next = { workspace = true, optional = true }
2930
fast_image_resize = { workspace = true, features = ["rayon"] }
3031

3132
[target.'cfg(target_os = "linux")'.dependencies]
@@ -51,11 +52,14 @@ env_logger.workspace = true
5152

5253
[features]
5354
default = ["wayland-wlr"]
54-
windows = ["openh264-video-encoder"]
55+
windows = ["ffmpeg-video-encoder"]
5556
wayland-wlr = ["dep:screen-capture-wayland-wlr", "x264-video-encoder"]
57+
wayland-portal = ["dep:screen-capture-wayland-portal", "x264-video-encoder"]
58+
5659
# For Debug on Linux
60+
# wayland-wlr = ["dep:screen-capture-wayland-wlr", "ffmpeg-video-encoder"]
5761
# wayland-wlr = ["dep:screen-capture-wayland-wlr", "openh264-video-encoder"]
58-
wayland-portal = ["dep:screen-capture-wayland-portal", "x264-video-encoder"]
5962

6063
x264-video-encoder = ["dep:x264"]
6164
openh264-video-encoder = ["dep:openh264"]
65+
ffmpeg-video-encoder = ["dep:ffmpeg-next"]

lib/recorder/src/video_encoder.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ mod ve_x264;
44
#[cfg(feature = "openh264-video-encoder")]
55
mod ve_openh264;
66

7+
#[cfg(feature = "ffmpeg-video-encoder")]
8+
mod ve_ffmpeg;
9+
710
use crate::{FPS, RecorderError, recorder::ResizedImageBuffer};
811
use derive_setters::Setters;
912

@@ -53,9 +56,13 @@ pub fn new(config: VideoEncoderConfig) -> Result<Box<dyn VideoEncoder>, Recorder
5356
#[cfg(feature = "openh264-video-encoder")]
5457
let ve = ve_openh264::OpenH264VideoEncoder::new(config)?;
5558

59+
#[cfg(feature = "ffmpeg-video-encoder")]
60+
let ve = ve_ffmpeg::FfmpegVideoEncoder::new(config)?;
61+
5662
Ok(Box::new(ve))
5763
}
5864

65+
#[allow(unused)]
5966
pub fn rgb_to_i420_yuv(rgb_data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, RecorderError> {
6067
use yuv::{
6168
YuvChromaSubsampling, YuvConversionMode, YuvPlanarImageMut, YuvRange, YuvStandardMatrix,
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
- `Linux` platform uses `x264` (~12ms)
2-
- `Windows` platform uses `openh264` (~25ms)
1+
- `Linux` platform uses `x264` (~10ms). Binding `C` lib.
2+
- `Windows` platform uses `openh264` (~50ms). Pure `Rust` crate.
3+
- `Linux` and `Windows` platform uses `ffmpeg-next` with `libx264` (~10ms). Binding `C` lib.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
use std::time::Duration;
2+
3+
use super::EncodedFrame;
4+
use crate::{RecorderError, VideoEncoder, VideoEncoderConfig, recorder::ResizedImageBuffer};
5+
use ffmpeg_next::{Dictionary, Rational, codec, encoder, format, frame, packet};
6+
7+
pub struct FfmpegVideoEncoder {
8+
width: u32,
9+
height: u32,
10+
frame_index: u64,
11+
encoder: encoder::Video,
12+
}
13+
14+
impl FfmpegVideoEncoder {
15+
pub fn new(config: VideoEncoderConfig) -> Result<Self, RecorderError> {
16+
assert!(config.width > 0 && config.height > 0);
17+
18+
ffmpeg_next::init().map_err(|e| {
19+
RecorderError::VideoEncodingFailed(format!("Failed to initialize ffmpeg: {}", e))
20+
})?;
21+
22+
let codec = encoder::find_by_name("libx264")
23+
.or_else(|| encoder::find(codec::Id::H264))
24+
.ok_or_else(|| {
25+
RecorderError::VideoEncodingFailed("H.264 encoder not found".to_string())
26+
})?;
27+
28+
let mut encoder = codec::Context::new_with_codec(codec)
29+
.encoder()
30+
.video()
31+
.map_err(|e| {
32+
RecorderError::VideoEncodingFailed(format!(
33+
"Failed to create encoder context: {}",
34+
e
35+
))
36+
})?;
37+
38+
encoder.set_width(config.width);
39+
encoder.set_height(config.height);
40+
encoder.set_format(format::Pixel::YUV420P);
41+
encoder.set_frame_rate(Some(Rational::new(config.fps.to_u32() as i32, 1)));
42+
encoder.set_time_base((1, config.fps.to_u32() as i32));
43+
44+
let mut opts = Dictionary::new();
45+
opts.set("preset", "superfast");
46+
opts.set("profile", "baseline");
47+
opts.set("crf", "23");
48+
opts.set("g", format!("{}", config.fps.to_u32()).as_str()); // max_keyframe_interval
49+
opts.set("tune", "zerolatency");
50+
opts.set("forced-idr", "1"); // Force keyframes more regularly
51+
52+
let x264_params = format!(
53+
"annexb={}:bframes=0:cabac=0:scenecut=0:keyint={}:keyint_min={}:rc_lookahead=0",
54+
if config.annexb { 1 } else { 0 },
55+
config.fps.to_u32(),
56+
config.fps.to_u32()
57+
);
58+
opts.set("x264-params", x264_params.as_str());
59+
60+
let encoder = encoder.open_with(opts).map_err(|e| {
61+
RecorderError::VideoEncodingFailed(format!("Failed to open encoder: {}", e))
62+
})?;
63+
64+
Ok(Self {
65+
width: config.width,
66+
height: config.height,
67+
encoder,
68+
frame_index: 0,
69+
})
70+
}
71+
72+
fn create_yuv_frame_from_i420(&self, i420_data: &[u8]) -> Result<frame::Video, RecorderError> {
73+
let mut output_frame = frame::Video::empty();
74+
output_frame.set_format(format::Pixel::YUV420P);
75+
output_frame.set_width(self.width);
76+
output_frame.set_height(self.height);
77+
78+
unsafe {
79+
output_frame.alloc(format::Pixel::YUV420P, self.width, self.height);
80+
}
81+
82+
// Copy I420 data to YUV420P frame planes
83+
let frame_size = (self.width * self.height) as usize;
84+
85+
// Y plane
86+
let y_plane = output_frame.data_mut(0);
87+
y_plane[..frame_size].copy_from_slice(&i420_data[0..frame_size]);
88+
89+
// U plane
90+
let u_plane = output_frame.data_mut(1);
91+
let u_size = frame_size / 4;
92+
u_plane[..u_size].copy_from_slice(&i420_data[frame_size..frame_size + u_size]);
93+
94+
// V plane
95+
let v_plane = output_frame.data_mut(2);
96+
v_plane[..u_size].copy_from_slice(&i420_data[frame_size + u_size..]);
97+
98+
Ok(output_frame)
99+
}
100+
}
101+
102+
impl VideoEncoder for FfmpegVideoEncoder {
103+
fn encode_frame(&mut self, img: ResizedImageBuffer) -> Result<EncodedFrame, RecorderError> {
104+
let (img_width, img_height) = img.dimensions();
105+
if img_width != self.width || img_height != self.height {
106+
return Err(RecorderError::ImageProcessingFailed(format!(
107+
"frame is already resize. current size: {}x{}. expect size: {}x{}",
108+
img_width, img_height, self.width, self.height
109+
)));
110+
}
111+
112+
let i420_data = super::rgb_to_i420_yuv(img.as_raw(), self.width, self.height)?;
113+
let mut output_frame = self.create_yuv_frame_from_i420(&i420_data)?;
114+
output_frame.set_pts(Some(self.frame_index as i64));
115+
116+
self.encoder.send_frame(&output_frame).map_err(|e| {
117+
RecorderError::VideoEncodingFailed(format!("FFmpeg encoding failed: {e}"))
118+
})?;
119+
120+
let mut packet = packet::Packet::empty();
121+
match self.encoder.receive_packet(&mut packet) {
122+
Ok(_) => {
123+
if let Some(data) = packet.data() {
124+
self.frame_index += 1;
125+
Ok(EncodedFrame::Frame((self.frame_index, data.to_vec())))
126+
} else {
127+
return Err(RecorderError::VideoEncodingFailed(
128+
"FFmpeg encoder encode data is empty".to_string(),
129+
));
130+
}
131+
}
132+
Err(ffmpeg_next::Error::Other { errno }) if errno == 11 => {
133+
return Err(RecorderError::VideoEncodingFailed(
134+
"FFmpeg encoder encode empty frame".to_string(),
135+
));
136+
}
137+
Err(ffmpeg_next::Error::Eof) => {
138+
return Err(RecorderError::VideoEncodingFailed(
139+
"FFmpeg encoder Eof".to_string(),
140+
));
141+
}
142+
Err(e) => {
143+
return Err(RecorderError::VideoEncodingFailed(format!(
144+
"FFmpeg receive packet failed: {e}"
145+
)));
146+
}
147+
}
148+
}
149+
150+
fn headers(&mut self) -> Result<Vec<u8>, RecorderError> {
151+
log::debug!("Encoding test frame to extract headers from FFmpeg");
152+
153+
// Create a test frame (black frame)
154+
let test_frame_data = vec![0u8; (self.width * self.height * 3) as usize];
155+
let test_img = image::RgbImage::from_raw(self.width, self.height, test_frame_data)
156+
.ok_or_else(|| {
157+
RecorderError::ImageProcessingFailed(
158+
"Failed to create test frame for header extraction".to_string(),
159+
)
160+
})?;
161+
162+
let i420_data = super::rgb_to_i420_yuv(test_img.as_raw(), self.width, self.height)?;
163+
let mut output_frame = self.create_yuv_frame_from_i420(&i420_data)?;
164+
output_frame.set_pts(Some(0));
165+
166+
// Send test frame to encoder
167+
self.encoder.send_frame(&output_frame).map_err(|e| {
168+
RecorderError::VideoEncodingFailed(format!("FFmpeg test frame encoding failed: {e}"))
169+
})?;
170+
171+
// Try to receive packet (should contain SPS/PPS headers)
172+
let mut packet = packet::Packet::empty();
173+
match self.encoder.receive_packet(&mut packet) {
174+
Ok(_) => {
175+
if let Some(data) = packet.data() {
176+
log::debug!(
177+
"Successfully extracted headers from FFmpeg test frame: {} bytes",
178+
data.len()
179+
);
180+
return Ok(data.to_vec());
181+
}
182+
}
183+
Err(ffmpeg_next::Error::Other { errno }) if errno == 11 => {
184+
log::warn!("FFmpeg encoder needs more frames to generate headers");
185+
}
186+
Err(e) => {
187+
return Err(RecorderError::VideoEncodingFailed(format!(
188+
"Failed to receive headers packet: {e}",
189+
)));
190+
}
191+
}
192+
193+
log::warn!("Could not extract headers from FFmpeg test frame, using empty headers");
194+
Ok(vec![])
195+
}
196+
197+
fn flush(
198+
mut self: Box<Self>,
199+
mut cb: Box<dyn FnMut(Vec<u8>) + 'static>,
200+
) -> Result<(), RecorderError> {
201+
let mut empty_count = 0;
202+
let max_empty_attempts = 3;
203+
204+
loop {
205+
let mut packet = packet::Packet::empty();
206+
match self.encoder.receive_packet(&mut packet) {
207+
Ok(_) => {
208+
empty_count += 1;
209+
210+
if let Some(data) = packet.data() {
211+
cb(data.to_vec());
212+
empty_count = 0;
213+
} else {
214+
if empty_count >= max_empty_attempts {
215+
log::debug!("FFmpeg encoder flush completed (empty data limit)");
216+
break;
217+
}
218+
}
219+
}
220+
Err(ffmpeg_next::Error::Eof) => {
221+
log::debug!("FFmpeg encoder flush completed (EOF)");
222+
break;
223+
}
224+
Err(ffmpeg_next::Error::Other { errno }) if errno == 11 => {
225+
empty_count += 1;
226+
if empty_count >= max_empty_attempts {
227+
log::debug!("FFmpeg encoder flush completed (EAGAIN limit)");
228+
break;
229+
}
230+
continue;
231+
}
232+
Err(e) => {
233+
return Err(RecorderError::VideoEncodingFailed(format!(
234+
"Failed to flush encoder: {e}"
235+
)));
236+
}
237+
}
238+
std::thread::sleep(Duration::from_millis(3));
239+
}
240+
241+
Ok(())
242+
}
243+
}

0 commit comments

Comments
 (0)