Skip to content

Commit 746e1fc

Browse files
committed
can record screen
1 parent 081bfd7 commit 746e1fc

File tree

2 files changed

+164
-3
lines changed

2 files changed

+164
-3
lines changed

crates/bevy_dev_tools/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev" }
1919
bevy_color = { path = "../bevy_color", version = "0.18.0-dev" }
2020
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.18.0-dev" }
2121
bevy_ecs = { path = "../bevy_ecs", version = "0.18.0-dev" }
22+
bevy_image = { path = "../bevy_image", version = "0.18.0-dev" }
2223
bevy_input = { path = "../bevy_input", version = "0.18.0-dev" }
2324
bevy_math = { path = "../bevy_math", version = "0.18.0-dev" }
2425
bevy_picking = { path = "../bevy_picking", version = "0.18.0-dev" }
@@ -37,6 +38,8 @@ serde = { version = "1.0", features = ["derive"], optional = true }
3738
ron = { version = "0.10", optional = true }
3839
tracing = { version = "0.1", default-features = false, features = ["std"] }
3940

41+
x264 = "*"
42+
4043
[lints]
4144
workspace = true
4245

crates/bevy_dev_tools/src/easy_screenshot.rs

Lines changed: 161 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
use std::time::{SystemTime, UNIX_EPOCH};
1+
use std::{
2+
fs::File,
3+
io::Write,
4+
sync::mpsc::channel,
5+
time::{SystemTime, UNIX_EPOCH},
6+
};
27

38
use bevy_app::{App, Plugin, Update};
49
use bevy_ecs::prelude::*;
5-
use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode};
6-
use bevy_render::view::screenshot::{save_to_disk, Screenshot};
10+
use bevy_image::Image;
11+
use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode, ButtonInput};
12+
use bevy_render::view::screenshot::{save_to_disk, Screenshot, ScreenshotCaptured};
13+
use bevy_time::Time;
714
use bevy_window::{PrimaryWindow, Window};
15+
use x264::{Colorspace, Encoder};
816

917
/// File format the screenshot will be saved in
1018
#[derive(Clone, Copy)]
@@ -65,3 +73,153 @@ impl Plugin for EasyScreenshotPlugin {
6573
);
6674
}
6775
}
76+
77+
/// Add this plugin to your app to enable easy screen recording.
78+
pub struct EasyScreenRecordPlugin {
79+
/// The key to toggle recording.
80+
pub toggle: KeyCode,
81+
}
82+
83+
impl Default for EasyScreenRecordPlugin {
84+
fn default() -> Self {
85+
EasyScreenRecordPlugin {
86+
toggle: KeyCode::Space,
87+
}
88+
}
89+
}
90+
91+
#[expect(
92+
clippy::large_enum_variant,
93+
reason = "Large variant happens a lot more often than the others"
94+
)]
95+
enum RecordCommand {
96+
Start(String),
97+
Stop,
98+
Frame(Image, f64),
99+
}
100+
101+
impl Plugin for EasyScreenRecordPlugin {
102+
fn build(&self, app: &mut App) {
103+
let (tx, rx) = channel::<RecordCommand>();
104+
105+
std::thread::spawn(move || {
106+
let mut encoder: Option<Encoder> = None;
107+
let mut file: Option<File> = None;
108+
let mut started = false;
109+
let mut first_frame_time = None;
110+
loop {
111+
let Ok(next) = rx.recv() else {
112+
break;
113+
};
114+
match next {
115+
RecordCommand::Start(name) => {
116+
started = true;
117+
file = Some(File::create(name).unwrap());
118+
first_frame_time = None;
119+
}
120+
RecordCommand::Stop => {
121+
if let Some(encoder) = encoder.take() {
122+
let mut flush = encoder.flush();
123+
let mut file = file.take().unwrap();
124+
while let Some(result) = flush.next() {
125+
let (data, _) = result.unwrap();
126+
file.write_all(data.entirety()).unwrap();
127+
}
128+
}
129+
started = false;
130+
}
131+
RecordCommand::Frame(image, frame_time) => {
132+
if first_frame_time.is_none() {
133+
first_frame_time = Some(frame_time);
134+
continue;
135+
}
136+
if started && encoder.is_none() {
137+
let mut new_encoder = Encoder::builder()
138+
.fps((1.0 / (frame_time - first_frame_time.unwrap())) as u32, 1)
139+
.build(Colorspace::RGB, image.width() as i32, image.height() as i32)
140+
.unwrap();
141+
142+
{
143+
let headers = new_encoder.headers().unwrap();
144+
file.as_mut()
145+
.unwrap()
146+
.write_all(headers.entirety())
147+
.unwrap();
148+
}
149+
150+
encoder = Some(new_encoder);
151+
}
152+
if let Some(encoder) = encoder.as_mut() {
153+
let (data, _) = encoder
154+
.encode(
155+
((frame_time - first_frame_time.unwrap()) * 1000.0) as i64,
156+
x264::Image::rgb(
157+
image.width() as i32,
158+
image.height() as i32,
159+
&image.try_into_dynamic().unwrap().to_rgb8(),
160+
),
161+
)
162+
.unwrap();
163+
file.as_mut().unwrap().write_all(data.entirety()).unwrap();
164+
}
165+
}
166+
}
167+
}
168+
});
169+
170+
app.add_systems(
171+
Update,
172+
(
173+
{
174+
let tx = tx.clone();
175+
move |window: Single<&Window, With<PrimaryWindow>>,
176+
mut recording: Local<bool>| {
177+
if *recording {
178+
tx.send(RecordCommand::Stop).unwrap();
179+
} else {
180+
let since_the_epoch = SystemTime::now()
181+
.duration_since(UNIX_EPOCH)
182+
.expect("time should go forward");
183+
184+
let filename =
185+
format!("{}-{}.h264", window.title, since_the_epoch.as_millis(),);
186+
tx.send(RecordCommand::Start(filename)).unwrap();
187+
}
188+
*recording = !*recording;
189+
}
190+
}
191+
.run_if(input_just_pressed(self.toggle)),
192+
{
193+
let tx = tx.clone();
194+
let toggle = self.toggle;
195+
move |mut commands: Commands,
196+
mut recording: Local<bool>,
197+
mut frame_count: Local<i32>,
198+
input: Res<ButtonInput<KeyCode>>| {
199+
if input.just_pressed(toggle) && !*recording {
200+
*recording = true;
201+
*frame_count = 0;
202+
} else if input.just_pressed(toggle) {
203+
*recording = false;
204+
}
205+
if *recording {
206+
*frame_count += 1;
207+
let tx = tx.clone();
208+
commands.spawn(Screenshot::primary_window()).observe(
209+
move |screenshot_captured: On<ScreenshotCaptured>,
210+
time: Res<Time>| {
211+
let img = screenshot_captured.image.clone();
212+
tx.send(RecordCommand::Frame(
213+
img,
214+
time.elapsed().as_secs_f64(),
215+
))
216+
.unwrap();
217+
},
218+
);
219+
}
220+
}
221+
},
222+
),
223+
);
224+
}
225+
}

0 commit comments

Comments
 (0)