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