Application lifetime events (suspend audio on Android) (#10158)
# Objective - Handle pausing audio when Android app is suspended ## Solution - This is the start of application lifetime events. They are mostly useful on mobile - Next version of winit should add a few more - When application is suspended, send an event to notify the application, and run the schedule one last time before actually suspending the app - Audio is now suspended too 🎉 https://github.com/bevyengine/bevy/assets/8672791/d74e2e09-ee29-4f40-adf2-36a0c064f94e --------- Co-authored-by: Marco Buono <418473+coreh@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									51c70bc98c
								
							
						
					
					
						commit
						7d504b89c3
					
				@ -330,3 +330,22 @@ pub struct WindowThemeChanged {
 | 
			
		||||
    /// The new system theme.
 | 
			
		||||
    pub theme: WindowTheme,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Application lifetime events
 | 
			
		||||
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
 | 
			
		||||
#[reflect(Debug, PartialEq)]
 | 
			
		||||
#[cfg_attr(
 | 
			
		||||
    feature = "serialize",
 | 
			
		||||
    derive(serde::Serialize, serde::Deserialize),
 | 
			
		||||
    reflect(Serialize, Deserialize)
 | 
			
		||||
)]
 | 
			
		||||
pub enum ApplicationLifetime {
 | 
			
		||||
    /// The application just started.
 | 
			
		||||
    Started,
 | 
			
		||||
    /// The application was suspended.
 | 
			
		||||
    ///
 | 
			
		||||
    /// On Android, applications have one frame to react to this event before being paused in the background.
 | 
			
		||||
    Suspended,
 | 
			
		||||
    /// The application was resumed.
 | 
			
		||||
    Resumed,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -98,7 +98,8 @@ impl Plugin for WindowPlugin {
 | 
			
		||||
            .add_event::<WindowBackendScaleFactorChanged>()
 | 
			
		||||
            .add_event::<FileDragAndDrop>()
 | 
			
		||||
            .add_event::<WindowMoved>()
 | 
			
		||||
            .add_event::<WindowThemeChanged>();
 | 
			
		||||
            .add_event::<WindowThemeChanged>()
 | 
			
		||||
            .add_event::<ApplicationLifetime>();
 | 
			
		||||
 | 
			
		||||
        if let Some(primary_window) = &self.primary_window {
 | 
			
		||||
            let initial_focus = app
 | 
			
		||||
@ -141,7 +142,8 @@ impl Plugin for WindowPlugin {
 | 
			
		||||
            .register_type::<WindowBackendScaleFactorChanged>()
 | 
			
		||||
            .register_type::<FileDragAndDrop>()
 | 
			
		||||
            .register_type::<WindowMoved>()
 | 
			
		||||
            .register_type::<WindowThemeChanged>();
 | 
			
		||||
            .register_type::<WindowThemeChanged>()
 | 
			
		||||
            .register_type::<ApplicationLifetime>();
 | 
			
		||||
 | 
			
		||||
        // Register window descriptor and related types
 | 
			
		||||
        app.register_type::<Window>()
 | 
			
		||||
 | 
			
		||||
@ -38,10 +38,10 @@ use bevy_utils::{
 | 
			
		||||
    Duration, Instant,
 | 
			
		||||
};
 | 
			
		||||
use bevy_window::{
 | 
			
		||||
    exit_on_all_closed, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime,
 | 
			
		||||
    ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged,
 | 
			
		||||
    WindowCloseRequested, WindowCreated, WindowDestroyed, WindowFocused, WindowMoved,
 | 
			
		||||
    WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
 | 
			
		||||
    exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved,
 | 
			
		||||
    FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window,
 | 
			
		||||
    WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed,
 | 
			
		||||
    WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
 | 
			
		||||
};
 | 
			
		||||
#[cfg(target_os = "android")]
 | 
			
		||||
use bevy_window::{PrimaryWindow, RawHandleWrapper};
 | 
			
		||||
@ -279,6 +279,7 @@ struct WindowAndInputEventWriters<'w> {
 | 
			
		||||
    window_moved: EventWriter<'w, WindowMoved>,
 | 
			
		||||
    window_theme_changed: EventWriter<'w, WindowThemeChanged>,
 | 
			
		||||
    window_destroyed: EventWriter<'w, WindowDestroyed>,
 | 
			
		||||
    lifetime: EventWriter<'w, ApplicationLifetime>,
 | 
			
		||||
    keyboard_input: EventWriter<'w, KeyboardInput>,
 | 
			
		||||
    character_input: EventWriter<'w, ReceivedCharacter>,
 | 
			
		||||
    mouse_button_input: EventWriter<'w, MouseButtonInput>,
 | 
			
		||||
@ -298,8 +299,8 @@ struct WindowAndInputEventWriters<'w> {
 | 
			
		||||
/// Persistent state that is used to run the [`App`] according to the current
 | 
			
		||||
/// [`UpdateMode`].
 | 
			
		||||
struct WinitAppRunnerState {
 | 
			
		||||
    /// Is `true` if the app is running and not suspended.
 | 
			
		||||
    is_active: bool,
 | 
			
		||||
    /// Current active state of the app.
 | 
			
		||||
    active: ActiveState,
 | 
			
		||||
    /// Is `true` if a new [`WindowEvent`] has been received since the last update.
 | 
			
		||||
    window_event_received: bool,
 | 
			
		||||
    /// Is `true` if the app has requested a redraw since the last update.
 | 
			
		||||
@ -312,10 +313,28 @@ struct WinitAppRunnerState {
 | 
			
		||||
    scheduled_update: Option<Instant>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq)]
 | 
			
		||||
enum ActiveState {
 | 
			
		||||
    NotYetStarted,
 | 
			
		||||
    Active,
 | 
			
		||||
    Suspended,
 | 
			
		||||
    WillSuspend,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ActiveState {
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn should_run(&self) -> bool {
 | 
			
		||||
        match self {
 | 
			
		||||
            ActiveState::NotYetStarted | ActiveState::Suspended => false,
 | 
			
		||||
            ActiveState::Active | ActiveState::WillSuspend => true,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for WinitAppRunnerState {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            is_active: false,
 | 
			
		||||
            active: ActiveState::NotYetStarted,
 | 
			
		||||
            window_event_received: false,
 | 
			
		||||
            redraw_requested: false,
 | 
			
		||||
            wait_elapsed: false,
 | 
			
		||||
@ -700,19 +719,23 @@ pub fn winit_runner(mut app: App) {
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            event::Event::Suspended => {
 | 
			
		||||
                runner_state.is_active = false;
 | 
			
		||||
                #[cfg(target_os = "android")]
 | 
			
		||||
                {
 | 
			
		||||
                    // Remove the `RawHandleWrapper` from the primary window.
 | 
			
		||||
                    // This will trigger the surface destruction.
 | 
			
		||||
                    let mut query = app.world.query_filtered::<Entity, With<PrimaryWindow>>();
 | 
			
		||||
                    let entity = query.single(&app.world);
 | 
			
		||||
                    app.world.entity_mut(entity).remove::<RawHandleWrapper>();
 | 
			
		||||
                    *control_flow = ControlFlow::Wait;
 | 
			
		||||
                }
 | 
			
		||||
                let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
 | 
			
		||||
                event_writers.lifetime.send(ApplicationLifetime::Suspended);
 | 
			
		||||
                // Mark the state as `WillSuspend`. This will let the schedule run one last time
 | 
			
		||||
                // before actually suspending to let the application react
 | 
			
		||||
                runner_state.active = ActiveState::WillSuspend;
 | 
			
		||||
            }
 | 
			
		||||
            event::Event::Resumed => {
 | 
			
		||||
                runner_state.is_active = true;
 | 
			
		||||
                let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
 | 
			
		||||
                match runner_state.active {
 | 
			
		||||
                    ActiveState::NotYetStarted => {
 | 
			
		||||
                        event_writers.lifetime.send(ApplicationLifetime::Started);
 | 
			
		||||
                    }
 | 
			
		||||
                    _ => {
 | 
			
		||||
                        event_writers.lifetime.send(ApplicationLifetime::Resumed);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                runner_state.active = ActiveState::Active;
 | 
			
		||||
                #[cfg(target_os = "android")]
 | 
			
		||||
                {
 | 
			
		||||
                    // Get windows that are cached but without raw handles. Those window were already created, but got their
 | 
			
		||||
@ -754,7 +777,20 @@ pub fn winit_runner(mut app: App) {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            event::Event::MainEventsCleared => {
 | 
			
		||||
                if runner_state.is_active {
 | 
			
		||||
                if runner_state.active.should_run() {
 | 
			
		||||
                    if runner_state.active == ActiveState::WillSuspend {
 | 
			
		||||
                        runner_state.active = ActiveState::Suspended;
 | 
			
		||||
                        #[cfg(target_os = "android")]
 | 
			
		||||
                        {
 | 
			
		||||
                            // Remove the `RawHandleWrapper` from the primary window.
 | 
			
		||||
                            // This will trigger the surface destruction.
 | 
			
		||||
                            let mut query =
 | 
			
		||||
                                app.world.query_filtered::<Entity, With<PrimaryWindow>>();
 | 
			
		||||
                            let entity = query.single(&app.world);
 | 
			
		||||
                            app.world.entity_mut(entity).remove::<RawHandleWrapper>();
 | 
			
		||||
                            *control_flow = ControlFlow::Wait;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    let (config, windows) = focused_windows_state.get(&app.world);
 | 
			
		||||
                    let focused = windows.iter().any(|window| window.focused);
 | 
			
		||||
                    let should_update = match config.update_mode(focused) {
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,11 @@
 | 
			
		||||
// type aliases tends to obfuscate code while offering no improvement in code cleanliness.
 | 
			
		||||
#![allow(clippy::type_complexity)]
 | 
			
		||||
 | 
			
		||||
use bevy::{input::touch::TouchPhase, prelude::*, window::WindowMode};
 | 
			
		||||
use bevy::{
 | 
			
		||||
    input::touch::TouchPhase,
 | 
			
		||||
    prelude::*,
 | 
			
		||||
    window::{ApplicationLifetime, WindowMode},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// the `bevy_main` proc_macro generates the required boilerplate for iOS and Android
 | 
			
		||||
#[bevy_main]
 | 
			
		||||
@ -17,7 +21,7 @@ fn main() {
 | 
			
		||||
        ..default()
 | 
			
		||||
    }))
 | 
			
		||||
    .add_systems(Startup, (setup_scene, setup_music))
 | 
			
		||||
    .add_systems(Update, (touch_camera, button_handler));
 | 
			
		||||
    .add_systems(Update, (touch_camera, button_handler, handle_lifetime));
 | 
			
		||||
 | 
			
		||||
    // MSAA makes some Android devices panic, this is under investigation
 | 
			
		||||
    // https://github.com/bevyengine/bevy/issues/8229
 | 
			
		||||
@ -161,3 +165,18 @@ fn setup_music(asset_server: Res<AssetServer>, mut commands: Commands) {
 | 
			
		||||
        settings: PlaybackSettings::LOOP,
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Pause audio when app goes into background and resume when it returns.
 | 
			
		||||
// This is handled by the OS on iOS, but not on Android.
 | 
			
		||||
fn handle_lifetime(
 | 
			
		||||
    mut lifetime_events: EventReader<ApplicationLifetime>,
 | 
			
		||||
    music_controller: Query<&AudioSink>,
 | 
			
		||||
) {
 | 
			
		||||
    for event in lifetime_events.read() {
 | 
			
		||||
        match event {
 | 
			
		||||
            ApplicationLifetime::Suspended => music_controller.single().pause(),
 | 
			
		||||
            ApplicationLifetime::Resumed => music_controller.single().play(),
 | 
			
		||||
            ApplicationLifetime::Started => (),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user