Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_dev_tools/src/easy_screenshot.rs
9328 views
1
#[cfg(feature = "screenrecording")]
2
use core::time::Duration;
3
use std::time::{SystemTime, UNIX_EPOCH};
4
5
use bevy_app::{App, Plugin, PostUpdate, Update};
6
use bevy_camera::Camera;
7
use bevy_ecs::prelude::*;
8
use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode};
9
use bevy_math::{Quat, StableInterpolate, Vec3};
10
use bevy_render::view::screenshot::{save_to_disk, Screenshot};
11
use bevy_time::Time;
12
use bevy_transform::{components::Transform, TransformSystems};
13
use bevy_window::{PrimaryWindow, Window};
14
#[cfg(all(not(target_os = "windows"), feature = "screenrecording"))]
15
pub use x264::{Preset, Tune};
16
17
/// File format the screenshot will be saved in
18
#[derive(Clone, Copy)]
19
pub enum ScreenshotFormat {
20
/// JPEG format
21
Jpeg,
22
/// PNG format
23
Png,
24
/// BMP format
25
Bmp,
26
}
27
28
/// Add this plugin to your app to enable easy screenshotting.
29
///
30
/// Add this plugin, press the key, and you have a screenshot 🎉
31
pub struct EasyScreenshotPlugin {
32
/// Key that will trigger a screenshot
33
pub trigger: KeyCode,
34
/// Format of the screenshot
35
///
36
/// The corresponding image format must be supported by bevy renderer
37
pub format: ScreenshotFormat,
38
}
39
40
impl Default for EasyScreenshotPlugin {
41
fn default() -> Self {
42
EasyScreenshotPlugin {
43
trigger: KeyCode::PrintScreen,
44
format: ScreenshotFormat::Png,
45
}
46
}
47
}
48
49
impl Plugin for EasyScreenshotPlugin {
50
fn build(&self, app: &mut App) {
51
let format = self.format;
52
app.add_systems(
53
Update,
54
(move |mut commands: Commands, window: Single<&Window, With<PrimaryWindow>>| {
55
let since_the_epoch = SystemTime::now()
56
.duration_since(UNIX_EPOCH)
57
.expect("time should go forward");
58
59
commands
60
.spawn(Screenshot::primary_window())
61
.observe(save_to_disk(format!(
62
"{}-{}.{}",
63
window.title,
64
since_the_epoch.as_millis(),
65
match format {
66
ScreenshotFormat::Jpeg => "jpg",
67
ScreenshotFormat::Png => "png",
68
ScreenshotFormat::Bmp => "bmp",
69
}
70
)));
71
})
72
.run_if(input_just_pressed(self.trigger)),
73
);
74
}
75
}
76
77
/// Placeholder
78
#[cfg(all(target_os = "windows", feature = "screenrecording"))]
79
pub enum Preset {
80
/// Placeholder
81
Ultrafast,
82
/// Placeholder
83
Superfast,
84
/// Placeholder
85
Veryfast,
86
/// Placeholder
87
Faster,
88
/// Placeholder
89
Fast,
90
/// Placeholder
91
Medium,
92
/// Placeholder
93
Slow,
94
/// Placeholder
95
Slower,
96
/// Placeholder
97
Veryslow,
98
/// Placeholder
99
Placebo,
100
}
101
102
/// Placeholder
103
#[cfg(all(target_os = "windows", feature = "screenrecording"))]
104
pub enum Tune {
105
/// Placeholder
106
None,
107
/// Placeholder
108
Film,
109
/// Placeholder
110
Animation,
111
/// Placeholder
112
Grain,
113
/// Placeholder
114
StillImage,
115
/// Placeholder
116
Psnr,
117
/// Placeholder
118
Ssim,
119
}
120
121
#[cfg(feature = "screenrecording")]
122
/// Add this plugin to your app to enable easy screen recording.
123
pub struct EasyScreenRecordPlugin {
124
/// The key to toggle recording.
125
pub toggle: KeyCode,
126
/// h264 encoder preset
127
pub preset: Preset,
128
/// h264 encoder tune
129
pub tune: Tune,
130
/// target frame time
131
pub frame_time: Duration,
132
}
133
134
#[cfg(feature = "screenrecording")]
135
impl Default for EasyScreenRecordPlugin {
136
fn default() -> Self {
137
EasyScreenRecordPlugin {
138
toggle: KeyCode::Space,
139
preset: Preset::Medium,
140
tune: Tune::Animation,
141
frame_time: Duration::from_millis(33),
142
}
143
}
144
}
145
146
#[cfg(feature = "screenrecording")]
147
/// Controls screen recording
148
#[derive(Message)]
149
pub enum RecordScreen {
150
/// Starts screen recording
151
Start,
152
/// Stops screen recording
153
Stop,
154
}
155
156
#[cfg(feature = "screenrecording")]
157
/// The [`Update`] systems that the [`EasyScreenRecordPlugin`] runs
158
/// to start and stop recording on user command and
159
/// to send frames to the thread that manages video file creation.
160
/// These systems manipulate [`virtual`](bevy_time::Virtual)
161
/// [`time`](bevy_time::Time) in order to capture frames for video.
162
///
163
/// If any application [`Update`] systems have behavior that depend
164
/// on virtual time and must be recorded, ensure that these systems run
165
/// [`after(EasyScreenRecordSystems)`](bevy_ecs::schedule::IntoScheduleConfigs::after).
166
/// The application may run slower on screen during recording,
167
/// but the video playback will be at normal speed.
168
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
169
pub struct EasyScreenRecordSystems;
170
171
#[cfg(feature = "screenrecording")]
172
impl Plugin for EasyScreenRecordPlugin {
173
#[cfg_attr(
174
target_os = "windows",
175
expect(unused_variables, reason = "not working on windows")
176
)]
177
fn build(&self, app: &mut App) {
178
#[cfg(target_os = "windows")]
179
{
180
tracing::warn!("Screen recording is not currently supported on Windows: see https://github.com/bevyengine/bevy/issues/22132");
181
}
182
#[cfg(not(target_os = "windows"))]
183
{
184
use bevy_image::Image;
185
use bevy_render::view::screenshot::ScreenshotCaptured;
186
use bevy_time::Time;
187
use std::{fs::File, io::Write, sync::mpsc::channel};
188
use tracing::info;
189
use x264::{Colorspace, Encoder, Setup};
190
191
enum RecordCommand {
192
Start(String, Preset, Tune),
193
Stop,
194
Frame(Image),
195
}
196
197
let (tx, rx) = channel::<RecordCommand>();
198
199
let frame_time = self.frame_time;
200
201
std::thread::spawn(move || {
202
let mut encoder: Option<Encoder> = None;
203
let mut setup = None;
204
let mut file: Option<File> = None;
205
let mut frame = 0;
206
loop {
207
let Ok(next) = rx.recv() else {
208
break;
209
};
210
match next {
211
RecordCommand::Start(name, preset, tune) => {
212
info!("starting recording at {}", name);
213
file = Some(File::create(name).unwrap());
214
setup = Some(Setup::preset(preset, tune, false, true).high());
215
}
216
RecordCommand::Stop => {
217
if let Some(encoder) = encoder.take() {
218
let mut flush = encoder.flush();
219
let mut file = file.take().unwrap();
220
while let Some(result) = flush.next() {
221
let (data, _) = result.unwrap();
222
file.write_all(data.entirety()).unwrap();
223
}
224
}
225
info!("finished processing video");
226
}
227
RecordCommand::Frame(image) => {
228
if let Some(setup) = setup.take() {
229
let mut new_encoder = setup
230
.fps((1000 / frame_time.as_millis()) as u32, 1)
231
.build(
232
Colorspace::RGB,
233
image.width() as i32,
234
image.height() as i32,
235
)
236
.unwrap();
237
let headers = new_encoder.headers().unwrap();
238
file.as_mut()
239
.unwrap()
240
.write_all(headers.entirety())
241
.unwrap();
242
encoder = Some(new_encoder);
243
}
244
if let Some(encoder) = encoder.as_mut() {
245
let pts = (frame_time.as_millis() * frame) as i64;
246
247
frame += 1;
248
let (data, _) = encoder
249
.encode(
250
pts,
251
x264::Image::rgb(
252
image.width() as i32,
253
image.height() as i32,
254
&image.try_into_dynamic().unwrap().to_rgb8(),
255
),
256
)
257
.unwrap();
258
file.as_mut().unwrap().write_all(data.entirety()).unwrap();
259
}
260
}
261
}
262
}
263
});
264
265
let frame_time = self.frame_time;
266
267
app.add_message::<RecordScreen>().add_systems(
268
Update,
269
(
270
(move |mut messages: MessageWriter<RecordScreen>,
271
mut recording: Local<bool>| {
272
*recording = !*recording;
273
if *recording {
274
messages.write(RecordScreen::Start);
275
} else {
276
messages.write(RecordScreen::Stop);
277
}
278
})
279
.run_if(input_just_pressed(self.toggle)),
280
{
281
let tx = tx.clone();
282
let preset = self.preset;
283
let tune = self.tune;
284
move |mut commands: Commands,
285
mut recording: Local<bool>,
286
mut messages: MessageReader<RecordScreen>,
287
window: Single<&Window, With<PrimaryWindow>>,
288
current_screenshot: Query<(), With<Screenshot>>,
289
mut virtual_time: ResMut<Time<bevy_time::Virtual>>| {
290
match messages.read().last() {
291
Some(RecordScreen::Start) => {
292
let since_the_epoch = SystemTime::now()
293
.duration_since(UNIX_EPOCH)
294
.expect("time should go forward");
295
let filename = format!(
296
"{}-{}.h264",
297
window.title,
298
since_the_epoch.as_millis(),
299
);
300
tx.send(RecordCommand::Start(filename, preset, tune))
301
.unwrap();
302
*recording = true;
303
virtual_time.pause();
304
}
305
Some(RecordScreen::Stop) => {
306
tx.send(RecordCommand::Stop).unwrap();
307
*recording = false;
308
virtual_time.unpause();
309
info!("stopped recording. still processing video");
310
}
311
_ => {}
312
}
313
if *recording && current_screenshot.single().is_err() {
314
let tx = tx.clone();
315
commands.spawn(Screenshot::primary_window()).observe(
316
move |screenshot_captured: On<ScreenshotCaptured>,
317
mut virtual_time: ResMut<Time<bevy_time::Virtual>>,
318
mut time: ResMut<Time<()>>| {
319
let img = screenshot_captured.image.clone();
320
tx.send(RecordCommand::Frame(img)).unwrap();
321
virtual_time.advance_by(frame_time);
322
*time = virtual_time.as_generic();
323
},
324
);
325
}
326
}
327
},
328
)
329
.chain()
330
.in_set(EasyScreenRecordSystems),
331
);
332
}
333
}
334
}
335
336
/// Plugin to move the camera smoothly according to the current time
337
pub struct EasyCameraMovementPlugin {
338
/// Decay rate for the camera movement
339
pub decay_rate: f32,
340
}
341
342
impl Default for EasyCameraMovementPlugin {
343
fn default() -> Self {
344
Self { decay_rate: 1.0 }
345
}
346
}
347
348
/// Move the camera to the given position
349
#[derive(Component)]
350
pub struct CameraMovement {
351
/// Target position for the camera movement
352
pub translation: Vec3,
353
/// Target rotation for the camera movement
354
pub rotation: Quat,
355
}
356
357
impl Plugin for EasyCameraMovementPlugin {
358
fn build(&self, app: &mut App) {
359
let decay_rate = self.decay_rate;
360
app.add_systems(
361
PostUpdate,
362
(move |mut query: Single<(&mut Transform, &CameraMovement), With<Camera>>,
363
time: Res<Time>| {
364
{
365
{
366
let target = query.1;
367
query.0.translation.smooth_nudge(
368
&target.translation,
369
decay_rate,
370
time.delta_secs(),
371
);
372
query.0.rotation.smooth_nudge(
373
&target.rotation,
374
decay_rate,
375
time.delta_secs(),
376
);
377
}
378
}
379
})
380
.before(TransformSystems::Propagate),
381
);
382
}
383
}
384
385