diff --git a/core/os/main_loop.cpp b/core/os/main_loop.cpp
index f348d58a50..c4a3a848b1 100644
--- a/core/os/main_loop.cpp
+++ b/core/os/main_loop.cpp
@@ -43,6 +43,8 @@ void MainLoop::_bind_methods() {
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_IN);
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_OUT);
BIND_CONSTANT(NOTIFICATION_TEXT_SERVER_CHANGED);
+ BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_ENTERED);
+ BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_EXITED);
ADD_SIGNAL(MethodInfo("on_request_permissions_result", PropertyInfo(Variant::STRING, "permission"), PropertyInfo(Variant::BOOL, "granted")));
diff --git a/core/os/main_loop.h b/core/os/main_loop.h
index d7e1d60f88..fdd0c2cfe7 100644
--- a/core/os/main_loop.h
+++ b/core/os/main_loop.h
@@ -57,6 +57,8 @@ public:
NOTIFICATION_APPLICATION_FOCUS_IN = 2016,
NOTIFICATION_APPLICATION_FOCUS_OUT = 2017,
NOTIFICATION_TEXT_SERVER_CHANGED = 2018,
+ NOTIFICATION_APPLICATION_PIP_MODE_ENTERED = 2019,
+ NOTIFICATION_APPLICATION_PIP_MODE_EXITED = 2020,
};
virtual void initialize();
diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml
index 6fb91f82b6..1a5dc36a61 100644
--- a/doc/classes/DisplayServer.xml
+++ b/doc/classes/DisplayServer.xml
@@ -1604,6 +1604,14 @@
[b]Note:[/b] This method is implemented on Android, iOS, macOS, Windows, and Linux (X11/Wayland).
+
+
+
+
+ Returns [code]true[/code] if the application is in picture-in-picture mode.
+ [b]Note:[/b] This method is implemented on Android.
+
+
@@ -1695,6 +1703,33 @@
Sets the current mouse mode. See also [method mouse_get_mode].
+
+
+
+
+ Enters picture-in-picture mode.
+ [b]Note:[/b] This method is implemented on Android.
+
+
+
+
+
+
+
+
+ Specifies the aspect ratio for picture-in-picture mode.
+ [b]Note:[/b] This method is implemented on Android.
+
+
+
+
+
+
+
+ Specifies whether picture-in-picture mode should be entered automatically when the application goes in the background.
+ [b]Note:[/b] This method is implemented on Android.
+
+
@@ -2753,6 +2788,9 @@
Display server supports HDR output. [b]macOS, iOS, visionOS, Windows[/b]
+
+ Display server supports putting the application in picture-in-picture mode. [b]Android[/b]
+
Unknown or custom role.
diff --git a/doc/classes/MainLoop.xml b/doc/classes/MainLoop.xml
index 227e1ad422..0bf53560b7 100644
--- a/doc/classes/MainLoop.xml
+++ b/doc/classes/MainLoop.xml
@@ -143,5 +143,11 @@
Notification received when text server is changed.
+
+ Notification received when the application enters picture-in-picture mode.
+
+
+ Notification received when the application exits picture-in-picture mode.
+
diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml
index 62fa2663c3..532859a28f 100644
--- a/doc/classes/Node.xml
+++ b/doc/classes/Node.xml
@@ -1323,6 +1323,12 @@
Notification received when the [TextServer] is changed.
+
+ Notification received when the application enters picture-in-picture mode.
+
+
+ Notification received when the application exits picture-in-picture mode.
+
Notification received when an accessibility information update is required.
diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp
index 265c3beea0..00d1bd9b05 100644
--- a/platform/android/display_server_android.cpp
+++ b/platform/android/display_server_android.cpp
@@ -72,6 +72,12 @@ bool DisplayServerAndroid::has_feature(DisplayServerEnums::Feature p_feature) co
return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU));
} break;
#endif
+ case DisplayServerEnums::FEATURE_PIP_MODE: {
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL_V(godot_java, false);
+ return godot_java->is_pip_mode_supported();
+ } break;
+
case DisplayServerEnums::FEATURE_CURSOR_SHAPE:
//case DisplayServerEnums::FEATURE_CUSTOM_CURSOR_SHAPE:
//case DisplayServerEnums::FEATURE_HIDPI:
@@ -1003,3 +1009,31 @@ void DisplayServerAndroid::set_icon(const Ref &p_icon) {
bool DisplayServerAndroid::is_window_transparency_available() const {
return GLOBAL_GET_CACHED(bool, "display/window/per_pixel_transparency/allowed");
}
+
+bool DisplayServerAndroid::is_in_pip_mode(DisplayServerEnums::WindowID p_window) {
+ ERR_FAIL_COND_V(p_window != DisplayServerEnums::MAIN_WINDOW_ID, false);
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL_V(godot_java, false);
+ return godot_java->is_in_pip_mode();
+}
+
+void DisplayServerAndroid::pip_mode_enter(DisplayServerEnums::WindowID p_window) {
+ ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL(godot_java);
+ godot_java->enter_pip_mode();
+}
+
+void DisplayServerAndroid::pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window) {
+ ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL(godot_java);
+ godot_java->set_pip_mode_aspect_ratio(p_numerator, p_denominator);
+}
+
+void DisplayServerAndroid::pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window) {
+ ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL(godot_java);
+ godot_java->set_auto_enter_pip_mode_on_background(p_auto_enter_on_background);
+}
diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h
index c4b3f82a58..1d3757a793 100644
--- a/platform/android/display_server_android.h
+++ b/platform/android/display_server_android.h
@@ -269,6 +269,11 @@ public:
virtual bool is_window_transparency_available() const override;
+ virtual bool is_in_pip_mode(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
+ virtual void pip_mode_enter(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
+ virtual void pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
+ virtual void pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
+
DisplayServerAndroid(const String &p_rendering_driver, DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, DisplayServerEnums::Context p_context, int64_t p_parent_window, Error &r_error);
~DisplayServerAndroid();
};
diff --git a/platform/android/java/app/src/main/AndroidManifest.xml b/platform/android/java/app/src/main/AndroidManifest.xml
index 41774516a4..f523b31767 100644
--- a/platform/android/java/app/src/main/AndroidManifest.xml
+++ b/platform/android/java/app/src/main/AndroidManifest.xml
@@ -35,6 +35,7 @@
android:launchMode="singleInstancePerTask"
android:excludeFromRecents="false"
android:exported="false"
+ android:supportsPictureInPicture="true"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustResize"
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
diff --git a/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java b/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java
index 4bd7359b8a..8573f71326 100644
--- a/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java
+++ b/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java
@@ -92,4 +92,9 @@ public class GodotApp extends GodotActivity {
super.onGodotForceQuit(instance);
}
}
+
+ @Override
+ protected boolean isPiPEnabled() {
+ return true;
+ }
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
index 45c8f5050d..286d31f442 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
@@ -30,12 +30,7 @@
package org.godotengine.editor
-import android.app.PictureInPictureParams
-import android.content.pm.PackageManager
-import android.graphics.Rect
-import android.os.Build
import android.os.Bundle
-import android.util.Log
import android.view.View
import androidx.annotation.CallSuper
import androidx.core.view.isVisible
@@ -55,7 +50,6 @@ open class GodotGame : BaseGodotGame() {
private val TAG = GodotGame::class.java.simpleName
}
- private val gameViewSourceRectHint = Rect()
private val expandGameMenuButton: View? by lazy { findViewById(R.id.game_menu_expand_button) }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -75,13 +69,6 @@ open class GodotGame : BaseGodotGame() {
gameMenuFragment?.expandGameMenu()
}
}
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val gameView = findViewById(R.id.godot_fragment_container)
- gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
- gameView.getGlobalVisibleRect(gameViewSourceRectHint)
- }
- }
}
override fun getCommandLine(): MutableList {
@@ -96,27 +83,7 @@ open class GodotGame : BaseGodotGame() {
return updatedArgs
}
- override fun enterPiPMode() {
- if (hasPiPSystemFeature()) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- builder.setSeamlessResizeEnabled(false)
- }
- setPictureInPictureParams(builder.build())
- }
-
- Log.v(TAG, "Entering PiP mode")
- enterPictureInPictureMode()
- }
- }
-
- /**
- * Returns true the if the device supports picture-in-picture (PiP).
- */
- protected fun hasPiPSystemFeature(): Boolean {
- return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
- }
+ override fun isPiPEnabled() = true
override fun shouldShowGameMenuBar(): Boolean {
return intent.getBooleanExtra(
@@ -127,21 +94,11 @@ open class GodotGame : BaseGodotGame() {
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
- Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
// Hide the game menu fragment when in PiP.
gameMenuContainer?.isVisible = !isInPictureInPictureMode
}
- override fun onStop() {
- super.onStop()
-
- if (isInPictureInPictureMode && !isFinishing) {
- // We get in this state when PiP is closed, so we terminate the activity.
- finish()
- }
- }
-
override fun getGodotAppLayout() = R.layout.godot_game_layout
override fun getEditorWindowInfo() = RUN_GAME_INFO
@@ -258,7 +215,7 @@ open class GodotGame : BaseGodotGame() {
override fun isCloseButtonEnabled() = !isHorizonOSDevice(applicationContext)
- override fun isPiPButtonEnabled() = hasPiPSystemFeature()
+ override fun isPiPButtonEnabled() = isPiPModeSupported()
override fun isMenuBarCollapsable() = true
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt
index d03c1439cc..5456cb231e 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt
@@ -140,7 +140,7 @@ class EmbeddedGodotGame : GodotGame() {
override fun isMenuBarCollapsable() = false
- override fun isAlwaysOnTopSupported() = hasPiPSystemFeature()
+ override fun isAlwaysOnTopSupported() = isPiPModeSupported()
override fun onFullScreenUpdated(enabled: Boolean) {
godot?.enableImmersiveMode(enabled)
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt
index 715f050877..5c40c83065 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt
@@ -47,6 +47,7 @@ import androidx.fragment.app.Fragment
import org.godotengine.editor.BaseGodotEditor
import org.godotengine.editor.BaseGodotEditor.Companion.SNACKBAR_SHOW_DURATION_MS
import org.godotengine.editor.R
+import org.godotengine.godot.feature.PictureInPictureProvider
import org.godotengine.godot.utils.DialogUtils
/**
@@ -65,7 +66,7 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
/**
* Used to be notified of events fired when interacting with the game menu.
*/
- interface GameMenuListener {
+ interface GameMenuListener : PictureInPictureProvider {
/**
* Kotlin representation of the RuntimeNodeSelect::SelectMode enum in 'scene/debugger/scene_debugger.h'.
@@ -109,7 +110,6 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
fun isGameEmbeddingSupported(): Boolean
fun embedGameOnPlay(embedded: Boolean)
- fun enterPiPMode() {}
fun minimizeGameWindow() {}
fun closeGameWindow() {}
diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt
index 921e083e8f..7c5adb2173 100644
--- a/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt
@@ -43,6 +43,7 @@ import android.hardware.Sensor
import android.hardware.SensorManager
import android.os.*
import android.util.Log
+import android.util.Rational
import android.util.TypedValue
import android.view.*
import android.widget.FrameLayout
@@ -57,6 +58,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.google.android.vending.expansion.downloader.*
import org.godotengine.godot.error.Error
+import org.godotengine.godot.feature.PictureInPictureProvider
import org.godotengine.godot.input.GodotEditText
import org.godotengine.godot.input.GodotInputHandler
import org.godotengine.godot.io.FilePicker
@@ -716,6 +718,13 @@ class Godot private constructor(val context: Context) {
}
}
+ internal fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
+ Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
+ runOnRenderThread {
+ GodotLib.onPictureInPictureModeChanged(isInPictureInPictureMode)
+ }
+ }
+
fun onPause(host: GodotHost) {
Log.v(TAG, "OnPause: $host")
resumed = false
@@ -1372,4 +1381,52 @@ class Godot private constructor(val context: Context) {
}
}
+ @Keep
+ private fun nativeIsPiPModeSupported(): Boolean {
+ val hostActivity = getActivity()
+ if (hostActivity is PictureInPictureProvider) {
+ return hostActivity.isPiPModeSupported()
+ }
+ return false
+ }
+
+ @Keep
+ private fun nativeIsInPiPMode(): Boolean {
+ val hostActivity = getActivity()
+ if (hostActivity is GodotActivity) {
+ return hostActivity.isInPictureInPictureMode
+ }
+ return false
+ }
+
+ @Keep
+ private fun nativeEnterPiPMode() {
+ val hostActivity = getActivity()
+ if (hostActivity is PictureInPictureProvider) {
+ runOnHostThread {
+ hostActivity.enterPiPMode()
+ }
+ }
+ }
+
+ @Keep
+ private fun nativeSetPiPModeAspectRatio(numerator: Int, denominator: Int) {
+ val hostActivity = getActivity()
+ if (hostActivity is GodotActivity) {
+ runOnHostThread {
+ hostActivity.updatePiPParams(aspectRatio = Rational(numerator, denominator))
+ }
+ }
+ }
+
+ @Keep
+ private fun nativeSetAutoEnterPiPModeOnBackground(autoEnterPiPOnBackground: Boolean) {
+ val hostActivity = getActivity()
+ if (hostActivity is GodotActivity) {
+ runOnHostThread {
+ hostActivity.updatePiPParams(enableAutoEnter = autoEnterPiPOnBackground)
+ }
+ }
+ }
+
}
diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt
index 56fedc4443..938bdb00b6 100644
--- a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt
+++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt
@@ -31,17 +31,25 @@
package org.godotengine.godot
import android.app.Activity
+import android.app.PictureInPictureParams
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
+import android.graphics.Rect
+import android.os.Build
import android.os.Bundle
import android.util.Log
+import android.util.Rational
+import android.view.View
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.fragment.app.FragmentActivity
+import org.godotengine.godot.feature.PictureInPictureProvider
import org.godotengine.godot.utils.CommandLineFileParser
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
/**
* Base abstract activity for Android apps intending to use Godot as the primary screen.
@@ -49,7 +57,7 @@ import org.godotengine.godot.utils.ProcessPhoenix
* Also a reference implementation for how to setup and use the [GodotFragment] fragment
* within an Android app.
*/
-abstract class GodotActivity : FragmentActivity(), GodotHost {
+abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPictureProvider {
companion object {
private val TAG = GodotActivity::class.java.simpleName
@@ -65,6 +73,12 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
private final val DEFAULT_WINDOW_ID = 664;
}
+ /**
+ * Set to true if the activity should automatically enter picture-in-picture when put in the background.
+ */
+ private val pipAspectRatio = AtomicReference()
+ private val autoEnterPiP = AtomicBoolean(false)
+ private val gameViewSourceRectHint = Rect()
private val commandLineParams = ArrayList()
/**
* Interaction with the [Godot] object is delegated to the [GodotFragment] class.
@@ -139,6 +153,13 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
.setPrimaryNavigationFragment(godotFragment)
.commitNowAllowingStateLoss()
}
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val gameView = findViewById(R.id.godot_fragment_container)
+ gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
+ gameView.getGlobalVisibleRect(gameViewSourceRectHint)
+ }
+ }
}
override fun onNewGodotInstanceRequested(args: Array): Int {
@@ -149,7 +170,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
.putExtra(EXTRA_COMMAND_LINE_PARAMS, args)
triggerRebirth(null, intent)
// fake 'process' id returned by create_instance() etc
- return DEFAULT_WINDOW_ID;
+ return DEFAULT_WINDOW_ID
}
protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
@@ -167,6 +188,15 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
super.onDestroy()
}
+ override fun onStop() {
+ super.onStop()
+
+ if (isInPictureInPictureMode && !isFinishing) {
+ // We get in this state when PiP is closed, so we terminate the activity.
+ finish()
+ }
+ }
+
override fun onGodotForceQuit(instance: Godot) {
runOnUiThread { terminateGodotInstance(instance) }
}
@@ -196,6 +226,23 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
}
}
+ override fun onGodotSetupCompleted() {
+ super.onGodotSetupCompleted()
+
+ if (isPiPEnabled()) {
+ try {
+ // Update the aspect ratio for picture-in-picture mode.
+ val viewportWidth = Integer.parseInt(GodotLib.getGlobal("display/window/size/viewport_width"))
+ val viewportHeight = Integer.parseInt(GodotLib.getGlobal("display/window/size/viewport_height"))
+ pipAspectRatio.set(Rational(viewportWidth, viewportHeight))
+ } catch (e: NumberFormatException) {
+ Log.w(TAG, "Unable to parse viewport dimensions.", e)
+ }
+
+ runOnHostThread { updatePiPParams() }
+ }
+ }
+
override fun onNewIntent(newIntent: Intent) {
intent = sanitizeLaunchIntent(newIntent)
super.onNewIntent(intent)
@@ -257,4 +304,56 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
@CallSuper
override fun getCommandLine(): MutableList = commandLineParams
+
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode)
+ godot?.onPictureInPictureModeChanged(isInPictureInPictureMode)
+ }
+
+ /**
+ * Returns true if picture-in-picture (PiP) mode is supported.
+ */
+ override fun isPiPModeSupported() = isPiPEnabled() && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+
+ /**
+ * Returns true if the current activity has enabled picture-in-picture in its manifest declaration using
+ * 'android:supportsPictureInPicture="true"'
+ */
+ protected open fun isPiPEnabled() = false
+
+ internal fun updatePiPParams(enableAutoEnter: Boolean = autoEnterPiP.get(), aspectRatio: Rational? = pipAspectRatio.get()) {
+ if (isPiPModeSupported()) {
+ autoEnterPiP.set(enableAutoEnter)
+ pipAspectRatio.set(aspectRatio)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val builder = PictureInPictureParams.Builder()
+ .setSourceRectHint(gameViewSourceRectHint)
+ .setAspectRatio(aspectRatio)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ builder.setSeamlessResizeEnabled(false)
+ .setAutoEnterEnabled(enableAutoEnter)
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ builder.setExpandedAspectRatio(aspectRatio)
+ }
+ setPictureInPictureParams(builder.build())
+ }
+ }
+ }
+
+ override fun enterPiPMode() {
+ if (isPiPModeSupported()) {
+ updatePiPParams()
+
+ Log.v(TAG, "Entering PiP mode")
+ enterPictureInPictureMode()
+ }
+ }
+
+ override fun onUserLeaveHint() {
+ if (autoEnterPiP.get()) {
+ enterPiPMode()
+ }
+ }
}
diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotLib.java
index 9984bca351..c6d780ceeb 100644
--- a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotLib.java
@@ -320,4 +320,6 @@ public class GodotLib {
static native boolean isProjectManagerHint();
static native boolean hasFeature(String feature);
+
+ static native void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
}
diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/feature/PictureInPictureProvider.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/feature/PictureInPictureProvider.kt
new file mode 100644
index 0000000000..c4979ce096
--- /dev/null
+++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/feature/PictureInPictureProvider.kt
@@ -0,0 +1,41 @@
+/**************************************************************************/
+/* PictureInPictureProvider.kt */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+package org.godotengine.godot.feature
+
+/**
+ * Provides APIs to enable picture-in-picture.
+ */
+interface PictureInPictureProvider {
+
+ fun enterPiPMode()
+
+ fun isPiPModeSupported(): Boolean
+}
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 65997dc7f6..e15f89c203 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -731,4 +731,18 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_hasFeature(JNIEnv
}
return false;
}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onPictureInPictureModeChanged(JNIEnv *env, jclass clazz, jboolean p_is_in_picture_in_picture_mode) {
+ if (step.get() <= STEP_SETUP) {
+ return;
+ }
+
+ if (os_android->get_main_loop()) {
+ if (p_is_in_picture_in_picture_mode) {
+ os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PIP_MODE_ENTERED);
+ } else {
+ os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PIP_MODE_EXITED);
+ }
+ }
+}
}
diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h
index 6c1faa0d5f..6579c5ca5d 100644
--- a/platform/android/java_godot_lib_jni.h
+++ b/platform/android/java_godot_lib_jni.h
@@ -78,4 +78,5 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResource
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_hasFeature(JNIEnv *env, jclass clazz, jstring p_feature);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onPictureInPictureModeChanged(JNIEnv *env, jclass clazz, jboolean p_is_in_picture_in_picture_mode);
}
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index fb1f467c43..ba880dc8b7 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -96,6 +96,13 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance) {
_build_env_execute = p_env->GetMethodID(godot_class, "nativeBuildEnvExecute", "(Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;Lorg/godotengine/godot/variant/Callable;)I");
_build_env_cancel = p_env->GetMethodID(godot_class, "nativeBuildEnvCancel", "(I)V");
_build_env_clean_project = p_env->GetMethodID(godot_class, "nativeBuildEnvCleanProject", "(Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;)V");
+
+ // PiP mode method ids.
+ _is_pip_mode_supported = p_env->GetMethodID(godot_class, "nativeIsPiPModeSupported", "()Z");
+ _is_in_pip_mode = p_env->GetMethodID(godot_class, "nativeIsInPiPMode", "()Z");
+ _enter_pip_mode = p_env->GetMethodID(godot_class, "nativeEnterPiPMode", "()V");
+ _set_pip_mode_aspect_ratio = p_env->GetMethodID(godot_class, "nativeSetPiPModeAspectRatio", "(II)V");
+ _set_auto_enter_pip_mode_on_background = p_env->GetMethodID(godot_class, "nativeSetAutoEnterPiPModeOnBackground", "(Z)V");
}
GodotJavaWrapper::~GodotJavaWrapper() {
@@ -705,3 +712,47 @@ void GodotJavaWrapper::build_env_clean_project(const String &p_project_path, con
env->DeleteLocalRef(j_callback);
}
}
+
+bool GodotJavaWrapper::is_pip_mode_supported() {
+ if (_is_pip_mode_supported) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, false);
+ return env->CallBooleanMethod(godot_instance, _is_pip_mode_supported);
+ } else {
+ return false;
+ }
+}
+
+bool GodotJavaWrapper::is_in_pip_mode() {
+ if (_is_in_pip_mode) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, false);
+ return env->CallBooleanMethod(godot_instance, _is_in_pip_mode);
+ } else {
+ return false;
+ }
+}
+
+void GodotJavaWrapper::enter_pip_mode() {
+ if (_enter_pip_mode) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+ env->CallVoidMethod(godot_instance, _enter_pip_mode);
+ }
+}
+
+void GodotJavaWrapper::set_pip_mode_aspect_ratio(int p_numerator, int p_denominator) {
+ if (_set_pip_mode_aspect_ratio) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+ env->CallVoidMethod(godot_instance, _set_pip_mode_aspect_ratio, p_numerator, p_denominator);
+ }
+}
+
+void GodotJavaWrapper::set_auto_enter_pip_mode_on_background(bool p_auto_enter_on_background) {
+ if (_set_auto_enter_pip_mode_on_background) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+ env->CallVoidMethod(godot_instance, _set_auto_enter_pip_mode_on_background, p_auto_enter_on_background);
+ }
+}
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index dfa98e0cc2..6e433eb4df 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -90,6 +90,11 @@ private:
jmethodID _build_env_execute = nullptr;
jmethodID _build_env_cancel = nullptr;
jmethodID _build_env_clean_project = nullptr;
+ jmethodID _is_pip_mode_supported = nullptr;
+ jmethodID _is_in_pip_mode = nullptr;
+ jmethodID _enter_pip_mode = nullptr;
+ jmethodID _set_pip_mode_aspect_ratio = nullptr;
+ jmethodID _set_auto_enter_pip_mode_on_background = nullptr;
public:
GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance);
@@ -154,4 +159,10 @@ public:
int build_env_execute(const String &p_build_tool, const List &p_arguments, const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_output_callback, const Callable &p_result_callback);
void build_env_cancel(int p_job_id);
void build_env_clean_project(const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_callback);
+
+ bool is_pip_mode_supported();
+ bool is_in_pip_mode();
+ void enter_pip_mode();
+ void set_pip_mode_aspect_ratio(int p_numerator, int p_denominator);
+ void set_auto_enter_pip_mode_on_background(bool p_auto_enter_on_background);
};
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index 4174c9e4e4..e62e0688e0 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -435,7 +435,9 @@ void OS_Android::main_loop_focusout() {
if (OS::get_singleton()->get_main_loop()) {
OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT);
}
- audio_driver_android.set_pause(true);
+
+ // Only pause when we are not in PiP mode.
+ audio_driver_android.set_pause(!DisplayServerAndroid::get_singleton()->is_in_pip_mode());
}
void OS_Android::main_loop_focusin() {
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index 6a2df1ae75..7bb91676cd 100644
--- a/scene/main/node.cpp
+++ b/scene/main/node.cpp
@@ -3964,6 +3964,8 @@ void Node::_bind_methods() {
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_IN);
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_OUT);
BIND_CONSTANT(NOTIFICATION_TEXT_SERVER_CHANGED);
+ BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_ENTERED);
+ BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_EXITED);
BIND_CONSTANT(NOTIFICATION_ACCESSIBILITY_UPDATE);
BIND_CONSTANT(NOTIFICATION_ACCESSIBILITY_INVALIDATE);
diff --git a/scene/main/node.h b/scene/main/node.h
index a06bd7dedb..8341daac47 100644
--- a/scene/main/node.h
+++ b/scene/main/node.h
@@ -498,6 +498,8 @@ public:
NOTIFICATION_APPLICATION_FOCUS_IN = 2016,
NOTIFICATION_APPLICATION_FOCUS_OUT = 2017,
NOTIFICATION_TEXT_SERVER_CHANGED = 2018,
+ NOTIFICATION_APPLICATION_PIP_MODE_ENTERED = 2019,
+ NOTIFICATION_APPLICATION_PIP_MODE_EXITED = 2020,
// Editor specific node notifications
NOTIFICATION_EDITOR_PRE_SAVE = 9001,
diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp
index 994a474bfe..d1c0ef42bd 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -920,7 +920,9 @@ void SceneTree::_notification(int p_notification) {
case NOTIFICATION_WM_ABOUT:
case NOTIFICATION_CRASH:
case NOTIFICATION_APPLICATION_RESUMED:
- case NOTIFICATION_APPLICATION_PAUSED: {
+ case NOTIFICATION_APPLICATION_PAUSED:
+ case NOTIFICATION_APPLICATION_PIP_MODE_ENTERED:
+ case NOTIFICATION_APPLICATION_PIP_MODE_EXITED: {
// Pass these to nodes, since they are mirrored.
get_root()->propagate_notification(p_notification);
} break;
diff --git a/servers/display/display_server.cpp b/servers/display/display_server.cpp
index 5804b596b5..cac5c9d2b3 100644
--- a/servers/display/display_server.cpp
+++ b/servers/display/display_server.cpp
@@ -1722,6 +1722,11 @@ void DisplayServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("unregister_additional_output", "object"), &DisplayServer::unregister_additional_output);
ClassDB::bind_method(D_METHOD("has_additional_outputs"), &DisplayServer::has_additional_outputs);
+ ClassDB::bind_method(D_METHOD("is_in_pip_mode", "window_id"), &DisplayServer::is_in_pip_mode, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
+ ClassDB::bind_method(D_METHOD("pip_mode_enter", "window_id"), &DisplayServer::pip_mode_enter, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
+ ClassDB::bind_method(D_METHOD("pip_mode_set_aspect_ratio", "numerator", "denominator", "window_id"), &DisplayServer::pip_mode_set_aspect_ratio, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
+ ClassDB::bind_method(D_METHOD("pip_mode_set_auto_enter_on_background", "auto_enter_on_background", "window_id"), &DisplayServer::pip_mode_set_auto_enter_on_background, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
+
#ifndef DISABLE_DEPRECATED
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_GLOBAL_MENU);
#endif // DISABLE_DEPRECATED
@@ -1759,6 +1764,7 @@ void DisplayServer::_bind_methods() {
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_SELF_FITTING_WINDOWS);
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_ACCESSIBILITY_SCREEN_READER);
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_HDR_OUTPUT);
+ BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_PIP_MODE);
#ifndef DISABLE_DEPRECATED
BIND_ENUM_CONSTANT(DisplayServerEnums::ROLE_UNKNOWN);
diff --git a/servers/display/display_server.h b/servers/display/display_server.h
index 00688f7c3b..f4ba35db89 100644
--- a/servers/display/display_server.h
+++ b/servers/display/display_server.h
@@ -531,6 +531,12 @@ public:
void unregister_additional_output(Object *p_output);
bool has_additional_outputs() const { return additional_outputs.size() > 0; }
+ /* PICTURE_IN_PICTURE */
+ virtual bool is_in_pip_mode(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) { return false; }
+ virtual void pip_mode_enter(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) {}
+ virtual void pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) {}
+ virtual void pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) {}
+
/* ACCESSIBILITY */
#ifndef DISABLE_DEPRECATED
diff --git a/servers/display/display_server_enums.h b/servers/display/display_server_enums.h
index a1604b9c3a..57e68ee759 100644
--- a/servers/display/display_server_enums.h
+++ b/servers/display/display_server_enums.h
@@ -79,6 +79,7 @@ enum Feature {
FEATURE_SELF_FITTING_WINDOWS,
FEATURE_ACCESSIBILITY_SCREEN_READER,
FEATURE_HDR_OUTPUT,
+ FEATURE_PIP_MODE,
};
/* RENDERING DEVICE */