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.
|
/// The new system theme.
|
||||||
pub theme: WindowTheme,
|
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::<WindowBackendScaleFactorChanged>()
|
||||||
.add_event::<FileDragAndDrop>()
|
.add_event::<FileDragAndDrop>()
|
||||||
.add_event::<WindowMoved>()
|
.add_event::<WindowMoved>()
|
||||||
.add_event::<WindowThemeChanged>();
|
.add_event::<WindowThemeChanged>()
|
||||||
|
.add_event::<ApplicationLifetime>();
|
||||||
|
|
||||||
if let Some(primary_window) = &self.primary_window {
|
if let Some(primary_window) = &self.primary_window {
|
||||||
let initial_focus = app
|
let initial_focus = app
|
||||||
@ -141,7 +142,8 @@ impl Plugin for WindowPlugin {
|
|||||||
.register_type::<WindowBackendScaleFactorChanged>()
|
.register_type::<WindowBackendScaleFactorChanged>()
|
||||||
.register_type::<FileDragAndDrop>()
|
.register_type::<FileDragAndDrop>()
|
||||||
.register_type::<WindowMoved>()
|
.register_type::<WindowMoved>()
|
||||||
.register_type::<WindowThemeChanged>();
|
.register_type::<WindowThemeChanged>()
|
||||||
|
.register_type::<ApplicationLifetime>();
|
||||||
|
|
||||||
// Register window descriptor and related types
|
// Register window descriptor and related types
|
||||||
app.register_type::<Window>()
|
app.register_type::<Window>()
|
||||||
|
|||||||
@ -38,10 +38,10 @@ use bevy_utils::{
|
|||||||
Duration, Instant,
|
Duration, Instant,
|
||||||
};
|
};
|
||||||
use bevy_window::{
|
use bevy_window::{
|
||||||
exit_on_all_closed, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime,
|
exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved,
|
||||||
ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged,
|
FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window,
|
||||||
WindowCloseRequested, WindowCreated, WindowDestroyed, WindowFocused, WindowMoved,
|
WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed,
|
||||||
WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
|
WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use bevy_window::{PrimaryWindow, RawHandleWrapper};
|
use bevy_window::{PrimaryWindow, RawHandleWrapper};
|
||||||
@ -279,6 +279,7 @@ struct WindowAndInputEventWriters<'w> {
|
|||||||
window_moved: EventWriter<'w, WindowMoved>,
|
window_moved: EventWriter<'w, WindowMoved>,
|
||||||
window_theme_changed: EventWriter<'w, WindowThemeChanged>,
|
window_theme_changed: EventWriter<'w, WindowThemeChanged>,
|
||||||
window_destroyed: EventWriter<'w, WindowDestroyed>,
|
window_destroyed: EventWriter<'w, WindowDestroyed>,
|
||||||
|
lifetime: EventWriter<'w, ApplicationLifetime>,
|
||||||
keyboard_input: EventWriter<'w, KeyboardInput>,
|
keyboard_input: EventWriter<'w, KeyboardInput>,
|
||||||
character_input: EventWriter<'w, ReceivedCharacter>,
|
character_input: EventWriter<'w, ReceivedCharacter>,
|
||||||
mouse_button_input: EventWriter<'w, MouseButtonInput>,
|
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
|
/// Persistent state that is used to run the [`App`] according to the current
|
||||||
/// [`UpdateMode`].
|
/// [`UpdateMode`].
|
||||||
struct WinitAppRunnerState {
|
struct WinitAppRunnerState {
|
||||||
/// Is `true` if the app is running and not suspended.
|
/// Current active state of the app.
|
||||||
is_active: bool,
|
active: ActiveState,
|
||||||
/// Is `true` if a new [`WindowEvent`] has been received since the last update.
|
/// Is `true` if a new [`WindowEvent`] has been received since the last update.
|
||||||
window_event_received: bool,
|
window_event_received: bool,
|
||||||
/// Is `true` if the app has requested a redraw since the last update.
|
/// Is `true` if the app has requested a redraw since the last update.
|
||||||
@ -312,10 +313,28 @@ struct WinitAppRunnerState {
|
|||||||
scheduled_update: Option<Instant>,
|
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 {
|
impl Default for WinitAppRunnerState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
is_active: false,
|
active: ActiveState::NotYetStarted,
|
||||||
window_event_received: false,
|
window_event_received: false,
|
||||||
redraw_requested: false,
|
redraw_requested: false,
|
||||||
wait_elapsed: false,
|
wait_elapsed: false,
|
||||||
@ -700,19 +719,23 @@ pub fn winit_runner(mut app: App) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
event::Event::Suspended => {
|
event::Event::Suspended => {
|
||||||
runner_state.is_active = false;
|
let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
|
||||||
#[cfg(target_os = "android")]
|
event_writers.lifetime.send(ApplicationLifetime::Suspended);
|
||||||
{
|
// Mark the state as `WillSuspend`. This will let the schedule run one last time
|
||||||
// Remove the `RawHandleWrapper` from the primary window.
|
// before actually suspending to let the application react
|
||||||
// This will trigger the surface destruction.
|
runner_state.active = ActiveState::WillSuspend;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
event::Event::Resumed => {
|
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")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
// Get windows that are cached but without raw handles. Those window were already created, but got their
|
// 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 => {
|
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 (config, windows) = focused_windows_state.get(&app.world);
|
||||||
let focused = windows.iter().any(|window| window.focused);
|
let focused = windows.iter().any(|window| window.focused);
|
||||||
let should_update = match config.update_mode(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.
|
// type aliases tends to obfuscate code while offering no improvement in code cleanliness.
|
||||||
#![allow(clippy::type_complexity)]
|
#![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
|
// the `bevy_main` proc_macro generates the required boilerplate for iOS and Android
|
||||||
#[bevy_main]
|
#[bevy_main]
|
||||||
@ -17,7 +21,7 @@ fn main() {
|
|||||||
..default()
|
..default()
|
||||||
}))
|
}))
|
||||||
.add_systems(Startup, (setup_scene, setup_music))
|
.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
|
// MSAA makes some Android devices panic, this is under investigation
|
||||||
// https://github.com/bevyengine/bevy/issues/8229
|
// https://github.com/bevyengine/bevy/issues/8229
|
||||||
@ -161,3 +165,18 @@ fn setup_music(asset_server: Res<AssetServer>, mut commands: Commands) {
|
|||||||
settings: PlaybackSettings::LOOP,
|
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