Add support for PiP mode

This commit is contained in:
Fredia Huya-Kouadio 2025-12-31 02:14:23 -08:00
parent 220b0b2f74
commit ef0163ba9f
27 changed files with 405 additions and 52 deletions

View file

@ -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")));

View file

@ -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();

View file

@ -1604,6 +1604,14 @@
[b]Note:[/b] This method is implemented on Android, iOS, macOS, Windows, and Linux (X11/Wayland).
</description>
</method>
<method name="is_in_pip_mode">
<return type="bool" />
<param index="0" name="window_id" type="int" default="0" />
<description>
Returns [code]true[/code] if the application is in picture-in-picture mode.
[b]Note:[/b] This method is implemented on Android.
</description>
</method>
<method name="is_touchscreen_available" qualifiers="const">
<return type="bool" />
<description>
@ -1695,6 +1703,33 @@
Sets the current mouse mode. See also [method mouse_get_mode].
</description>
</method>
<method name="pip_mode_enter">
<return type="void" />
<param index="0" name="window_id" type="int" default="0" />
<description>
Enters picture-in-picture mode.
[b]Note:[/b] This method is implemented on Android.
</description>
</method>
<method name="pip_mode_set_aspect_ratio">
<return type="void" />
<param index="0" name="numerator" type="int" />
<param index="1" name="denominator" type="int" />
<param index="2" name="window_id" type="int" default="0" />
<description>
Specifies the aspect ratio for picture-in-picture mode.
[b]Note:[/b] This method is implemented on Android.
</description>
</method>
<method name="pip_mode_set_auto_enter_on_background">
<return type="void" />
<param index="0" name="auto_enter_on_background" type="bool" />
<param index="1" name="window_id" type="int" default="0" />
<description>
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.
</description>
</method>
<method name="process_events">
<return type="void" />
<description>
@ -2753,6 +2788,9 @@
<constant name="FEATURE_HDR_OUTPUT" value="35" enum="Feature">
Display server supports HDR output. [b]macOS, iOS, visionOS, Windows[/b]
</constant>
<constant name="FEATURE_PIP_MODE" value="36" enum="Feature">
Display server supports putting the application in picture-in-picture mode. [b]Android[/b]
</constant>
<constant name="ROLE_UNKNOWN" value="0" enum="AccessibilityRole" deprecated="Use [AccessibilityServer] instead.">
Unknown or custom role.
</constant>

View file

@ -143,5 +143,11 @@
<constant name="NOTIFICATION_TEXT_SERVER_CHANGED" value="2018">
Notification received when text server is changed.
</constant>
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_ENTERED" value="2019">
Notification received when the application enters picture-in-picture mode.
</constant>
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_EXITED" value="2020">
Notification received when the application exits picture-in-picture mode.
</constant>
</constants>
</class>

View file

@ -1323,6 +1323,12 @@
<constant name="NOTIFICATION_TEXT_SERVER_CHANGED" value="2018">
Notification received when the [TextServer] is changed.
</constant>
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_ENTERED" value="2019">
Notification received when the application enters picture-in-picture mode.
</constant>
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_EXITED" value="2020">
Notification received when the application exits picture-in-picture mode.
</constant>
<constant name="NOTIFICATION_ACCESSIBILITY_UPDATE" value="3000">
Notification received when an accessibility information update is required.
</constant>

View file

@ -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<Image> &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);
}

View file

@ -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();
};

View file

@ -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"

View file

@ -92,4 +92,9 @@ public class GodotApp extends GodotActivity {
super.onGodotForceQuit(instance);
}
}
@Override
protected boolean isPiPEnabled() {
return true;
}
}

View file

@ -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<View>(R.id.godot_fragment_container)
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
}
}
}
override fun getCommandLine(): MutableList<String> {
@ -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

View file

@ -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)

View file

@ -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() {}

View file

@ -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)
}
}
}
}

View file

@ -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<Rational>()
private val autoEnterPiP = AtomicBoolean(false)
private val gameViewSourceRectHint = Rect()
private val commandLineParams = ArrayList<String>()
/**
* 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<View>(R.id.godot_fragment_container)
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
}
}
}
override fun onNewGodotInstanceRequested(args: Array<String>): 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<String> = 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()
}
}
}

View file

@ -320,4 +320,6 @@ public class GodotLib {
static native boolean isProjectManagerHint();
static native boolean hasFeature(String feature);
static native void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
}

View file

@ -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
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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<String> &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);
};

View file

@ -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() {

View file

@ -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);

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -79,6 +79,7 @@ enum Feature {
FEATURE_SELF_FITTING_WINDOWS,
FEATURE_ACCESSIBILITY_SCREEN_READER,
FEATURE_HDR_OUTPUT,
FEATURE_PIP_MODE,
};
/* RENDERING DEVICE */