Path: blob/main/crates/bevy_dev_tools/src/easy_screenshot.rs
9328 views
#[cfg(feature = "screenrecording")]1use core::time::Duration;2use std::time::{SystemTime, UNIX_EPOCH};34use bevy_app::{App, Plugin, PostUpdate, Update};5use bevy_camera::Camera;6use bevy_ecs::prelude::*;7use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode};8use bevy_math::{Quat, StableInterpolate, Vec3};9use bevy_render::view::screenshot::{save_to_disk, Screenshot};10use bevy_time::Time;11use bevy_transform::{components::Transform, TransformSystems};12use bevy_window::{PrimaryWindow, Window};13#[cfg(all(not(target_os = "windows"), feature = "screenrecording"))]14pub use x264::{Preset, Tune};1516/// File format the screenshot will be saved in17#[derive(Clone, Copy)]18pub enum ScreenshotFormat {19/// JPEG format20Jpeg,21/// PNG format22Png,23/// BMP format24Bmp,25}2627/// Add this plugin to your app to enable easy screenshotting.28///29/// Add this plugin, press the key, and you have a screenshot 🎉30pub struct EasyScreenshotPlugin {31/// Key that will trigger a screenshot32pub trigger: KeyCode,33/// Format of the screenshot34///35/// The corresponding image format must be supported by bevy renderer36pub format: ScreenshotFormat,37}3839impl Default for EasyScreenshotPlugin {40fn default() -> Self {41EasyScreenshotPlugin {42trigger: KeyCode::PrintScreen,43format: ScreenshotFormat::Png,44}45}46}4748impl Plugin for EasyScreenshotPlugin {49fn build(&self, app: &mut App) {50let format = self.format;51app.add_systems(52Update,53(move |mut commands: Commands, window: Single<&Window, With<PrimaryWindow>>| {54let since_the_epoch = SystemTime::now()55.duration_since(UNIX_EPOCH)56.expect("time should go forward");5758commands59.spawn(Screenshot::primary_window())60.observe(save_to_disk(format!(61"{}-{}.{}",62window.title,63since_the_epoch.as_millis(),64match format {65ScreenshotFormat::Jpeg => "jpg",66ScreenshotFormat::Png => "png",67ScreenshotFormat::Bmp => "bmp",68}69)));70})71.run_if(input_just_pressed(self.trigger)),72);73}74}7576/// Placeholder77#[cfg(all(target_os = "windows", feature = "screenrecording"))]78pub enum Preset {79/// Placeholder80Ultrafast,81/// Placeholder82Superfast,83/// Placeholder84Veryfast,85/// Placeholder86Faster,87/// Placeholder88Fast,89/// Placeholder90Medium,91/// Placeholder92Slow,93/// Placeholder94Slower,95/// Placeholder96Veryslow,97/// Placeholder98Placebo,99}100101/// Placeholder102#[cfg(all(target_os = "windows", feature = "screenrecording"))]103pub enum Tune {104/// Placeholder105None,106/// Placeholder107Film,108/// Placeholder109Animation,110/// Placeholder111Grain,112/// Placeholder113StillImage,114/// Placeholder115Psnr,116/// Placeholder117Ssim,118}119120#[cfg(feature = "screenrecording")]121/// Add this plugin to your app to enable easy screen recording.122pub struct EasyScreenRecordPlugin {123/// The key to toggle recording.124pub toggle: KeyCode,125/// h264 encoder preset126pub preset: Preset,127/// h264 encoder tune128pub tune: Tune,129/// target frame time130pub frame_time: Duration,131}132133#[cfg(feature = "screenrecording")]134impl Default for EasyScreenRecordPlugin {135fn default() -> Self {136EasyScreenRecordPlugin {137toggle: KeyCode::Space,138preset: Preset::Medium,139tune: Tune::Animation,140frame_time: Duration::from_millis(33),141}142}143}144145#[cfg(feature = "screenrecording")]146/// Controls screen recording147#[derive(Message)]148pub enum RecordScreen {149/// Starts screen recording150Start,151/// Stops screen recording152Stop,153}154155#[cfg(feature = "screenrecording")]156/// The [`Update`] systems that the [`EasyScreenRecordPlugin`] runs157/// to start and stop recording on user command and158/// to send frames to the thread that manages video file creation.159/// These systems manipulate [`virtual`](bevy_time::Virtual)160/// [`time`](bevy_time::Time) in order to capture frames for video.161///162/// If any application [`Update`] systems have behavior that depend163/// on virtual time and must be recorded, ensure that these systems run164/// [`after(EasyScreenRecordSystems)`](bevy_ecs::schedule::IntoScheduleConfigs::after).165/// The application may run slower on screen during recording,166/// but the video playback will be at normal speed.167#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]168pub struct EasyScreenRecordSystems;169170#[cfg(feature = "screenrecording")]171impl Plugin for EasyScreenRecordPlugin {172#[cfg_attr(173target_os = "windows",174expect(unused_variables, reason = "not working on windows")175)]176fn build(&self, app: &mut App) {177#[cfg(target_os = "windows")]178{179tracing::warn!("Screen recording is not currently supported on Windows: see https://github.com/bevyengine/bevy/issues/22132");180}181#[cfg(not(target_os = "windows"))]182{183use bevy_image::Image;184use bevy_render::view::screenshot::ScreenshotCaptured;185use bevy_time::Time;186use std::{fs::File, io::Write, sync::mpsc::channel};187use tracing::info;188use x264::{Colorspace, Encoder, Setup};189190enum RecordCommand {191Start(String, Preset, Tune),192Stop,193Frame(Image),194}195196let (tx, rx) = channel::<RecordCommand>();197198let frame_time = self.frame_time;199200std::thread::spawn(move || {201let mut encoder: Option<Encoder> = None;202let mut setup = None;203let mut file: Option<File> = None;204let mut frame = 0;205loop {206let Ok(next) = rx.recv() else {207break;208};209match next {210RecordCommand::Start(name, preset, tune) => {211info!("starting recording at {}", name);212file = Some(File::create(name).unwrap());213setup = Some(Setup::preset(preset, tune, false, true).high());214}215RecordCommand::Stop => {216if let Some(encoder) = encoder.take() {217let mut flush = encoder.flush();218let mut file = file.take().unwrap();219while let Some(result) = flush.next() {220let (data, _) = result.unwrap();221file.write_all(data.entirety()).unwrap();222}223}224info!("finished processing video");225}226RecordCommand::Frame(image) => {227if let Some(setup) = setup.take() {228let mut new_encoder = setup229.fps((1000 / frame_time.as_millis()) as u32, 1)230.build(231Colorspace::RGB,232image.width() as i32,233image.height() as i32,234)235.unwrap();236let headers = new_encoder.headers().unwrap();237file.as_mut()238.unwrap()239.write_all(headers.entirety())240.unwrap();241encoder = Some(new_encoder);242}243if let Some(encoder) = encoder.as_mut() {244let pts = (frame_time.as_millis() * frame) as i64;245246frame += 1;247let (data, _) = encoder248.encode(249pts,250x264::Image::rgb(251image.width() as i32,252image.height() as i32,253&image.try_into_dynamic().unwrap().to_rgb8(),254),255)256.unwrap();257file.as_mut().unwrap().write_all(data.entirety()).unwrap();258}259}260}261}262});263264let frame_time = self.frame_time;265266app.add_message::<RecordScreen>().add_systems(267Update,268(269(move |mut messages: MessageWriter<RecordScreen>,270mut recording: Local<bool>| {271*recording = !*recording;272if *recording {273messages.write(RecordScreen::Start);274} else {275messages.write(RecordScreen::Stop);276}277})278.run_if(input_just_pressed(self.toggle)),279{280let tx = tx.clone();281let preset = self.preset;282let tune = self.tune;283move |mut commands: Commands,284mut recording: Local<bool>,285mut messages: MessageReader<RecordScreen>,286window: Single<&Window, With<PrimaryWindow>>,287current_screenshot: Query<(), With<Screenshot>>,288mut virtual_time: ResMut<Time<bevy_time::Virtual>>| {289match messages.read().last() {290Some(RecordScreen::Start) => {291let since_the_epoch = SystemTime::now()292.duration_since(UNIX_EPOCH)293.expect("time should go forward");294let filename = format!(295"{}-{}.h264",296window.title,297since_the_epoch.as_millis(),298);299tx.send(RecordCommand::Start(filename, preset, tune))300.unwrap();301*recording = true;302virtual_time.pause();303}304Some(RecordScreen::Stop) => {305tx.send(RecordCommand::Stop).unwrap();306*recording = false;307virtual_time.unpause();308info!("stopped recording. still processing video");309}310_ => {}311}312if *recording && current_screenshot.single().is_err() {313let tx = tx.clone();314commands.spawn(Screenshot::primary_window()).observe(315move |screenshot_captured: On<ScreenshotCaptured>,316mut virtual_time: ResMut<Time<bevy_time::Virtual>>,317mut time: ResMut<Time<()>>| {318let img = screenshot_captured.image.clone();319tx.send(RecordCommand::Frame(img)).unwrap();320virtual_time.advance_by(frame_time);321*time = virtual_time.as_generic();322},323);324}325}326},327)328.chain()329.in_set(EasyScreenRecordSystems),330);331}332}333}334335/// Plugin to move the camera smoothly according to the current time336pub struct EasyCameraMovementPlugin {337/// Decay rate for the camera movement338pub decay_rate: f32,339}340341impl Default for EasyCameraMovementPlugin {342fn default() -> Self {343Self { decay_rate: 1.0 }344}345}346347/// Move the camera to the given position348#[derive(Component)]349pub struct CameraMovement {350/// Target position for the camera movement351pub translation: Vec3,352/// Target rotation for the camera movement353pub rotation: Quat,354}355356impl Plugin for EasyCameraMovementPlugin {357fn build(&self, app: &mut App) {358let decay_rate = self.decay_rate;359app.add_systems(360PostUpdate,361(move |mut query: Single<(&mut Transform, &CameraMovement), With<Camera>>,362time: Res<Time>| {363{364{365let target = query.1;366query.0.translation.smooth_nudge(367&target.translation,368decay_rate,369time.delta_secs(),370);371query.0.rotation.smooth_nudge(372&target.rotation,373decay_rate,374time.delta_secs(),375);376}377}378})379.before(TransformSystems::Propagate),380);381}382}383384385