Skip to content

Commit

Permalink
Application lifetime events (suspend audio on Android) (bevyengine#10158
Browse files Browse the repository at this point in the history
)

# 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>
  • Loading branch information
2 people authored and Ray Redondo committed Jan 9, 2024
1 parent 22c320a commit 107be66
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 23 deletions.
19 changes: 19 additions & 0 deletions crates/bevy_window/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
6 changes: 4 additions & 2 deletions crates/bevy_window/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>()
Expand Down
74 changes: 55 additions & 19 deletions crates/bevy_winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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>,
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
23 changes: 21 additions & 2 deletions examples/mobile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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 => (),
}
}
}

0 comments on commit 107be66

Please sign in to comment.