[RN] Add Picture-in-Picture support
This only works automatically on Android >= 8. On other platforms / versions, it relies on the SDK user on implementing a "reduced UI" mode and reacting to the "request PIP" delegate method.
This commit is contained in:
parent
94473e5660
commit
b3683068d4
|
@ -118,8 +118,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onStop() {
|
||||||
super.onPause();
|
super.onStop();
|
||||||
|
|
||||||
JitsiMeetView.onHostPause(this);
|
JitsiMeetView.onHostPause(this);
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,10 @@ which displays a single `JitsiMeetView`.
|
||||||
|
|
||||||
See JitsiMeetView.getDefaultURL.
|
See JitsiMeetView.getDefaultURL.
|
||||||
|
|
||||||
|
#### getPictureInPictureAvailable()
|
||||||
|
|
||||||
|
See JitsiMeetView.getPictureInPictureAvailable.
|
||||||
|
|
||||||
#### getWelcomePageEnabled()
|
#### getWelcomePageEnabled()
|
||||||
|
|
||||||
See JitsiMeetView.getWelcomePageEnabled.
|
See JitsiMeetView.getWelcomePageEnabled.
|
||||||
|
@ -154,6 +158,10 @@ See JitsiMeetView.loadURL.
|
||||||
|
|
||||||
See JitsiMeetView.setDefaultURL.
|
See JitsiMeetView.setDefaultURL.
|
||||||
|
|
||||||
|
#### setPictureInPictureAvailable(Boolean)
|
||||||
|
|
||||||
|
See JitsiMeetView.setPictureInPictureAvailable.
|
||||||
|
|
||||||
#### setWelcomePageEnabled(boolean)
|
#### setWelcomePageEnabled(boolean)
|
||||||
|
|
||||||
See JitsiMeetView.setWelcomePageEnabled.
|
See JitsiMeetView.setWelcomePageEnabled.
|
||||||
|
@ -179,6 +187,12 @@ if set to `null`, the default built in JavaScript is used: https://meet.jit.si.
|
||||||
|
|
||||||
Returns the `JitsiMeetViewListener` instance attached to the view.
|
Returns the `JitsiMeetViewListener` instance attached to the view.
|
||||||
|
|
||||||
|
#### getPictureInPictureAvailable()
|
||||||
|
|
||||||
|
turns true if Picture-in-Picture is available, false otherwise. If the user
|
||||||
|
doesn't explicitly set it, it will default to true if the platform supports it,
|
||||||
|
false otherwise. See the Picture-in-Picture section.
|
||||||
|
|
||||||
#### getWelcomePageEnabled()
|
#### getWelcomePageEnabled()
|
||||||
|
|
||||||
Returns true if the Welcome page is enabled; otherwise, false. If false, a black
|
Returns true if the Welcome page is enabled; otherwise, false. If false, a black
|
||||||
|
@ -227,6 +241,13 @@ NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
|
||||||
Sets the given listener (class implementing the `JitsiMeetViewListener`
|
Sets the given listener (class implementing the `JitsiMeetViewListener`
|
||||||
interface) on the view.
|
interface) on the view.
|
||||||
|
|
||||||
|
#### setPictureInPictureAvailable(Boolean)
|
||||||
|
|
||||||
|
Sets wether Picture-in-Picture is available. When set to `null` if will be
|
||||||
|
detected at runtime based on platform support.
|
||||||
|
|
||||||
|
NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
|
||||||
|
|
||||||
#### setWelcomePageEnabled(boolean)
|
#### setWelcomePageEnabled(boolean)
|
||||||
|
|
||||||
Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
|
Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
|
||||||
|
@ -257,7 +278,8 @@ This is a static method.
|
||||||
|
|
||||||
#### onHostResume(activity)
|
#### onHostResume(activity)
|
||||||
|
|
||||||
Helper method which should be called from the activity's `onResume` method.
|
Helper method which should be called from the activity's `onResume` or `onStop`
|
||||||
|
method.
|
||||||
|
|
||||||
This is a static method.
|
This is a static method.
|
||||||
|
|
||||||
|
@ -269,6 +291,13 @@ activity's `onNewIntent` method.
|
||||||
|
|
||||||
This is a static method.
|
This is a static method.
|
||||||
|
|
||||||
|
#### onUserLeaveHint()
|
||||||
|
|
||||||
|
Helper method for integrating automatic Picture-in-Picture. It should be called
|
||||||
|
from the activity's `onUserLeaveHint` method.
|
||||||
|
|
||||||
|
This is a static method.
|
||||||
|
|
||||||
#### JitsiMeetViewListener
|
#### JitsiMeetViewListener
|
||||||
|
|
||||||
`JitsiMeetViewListener` provides an interface apps can implement to listen to
|
`JitsiMeetViewListener` provides an interface apps can implement to listen to
|
||||||
|
@ -385,3 +414,15 @@ rules file:
|
||||||
-keep class org.jitsi.meet.sdk.** { *; }
|
-keep class org.jitsi.meet.sdk.** { *; }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Picture-in-Picture
|
||||||
|
|
||||||
|
The Jitsi Meet app and SDK will enable Android's native Picture-in-Picture mode
|
||||||
|
iff the platform is supported: for Android >= Oreo.
|
||||||
|
|
||||||
|
If the SDK is integrated in an application which calls
|
||||||
|
`enterPictureInPictureMode` for the Jitsi Meet activity, the it will self-adjust
|
||||||
|
by removing some UI elements.
|
||||||
|
|
||||||
|
Alternatively, this can be explicitly disabled with the
|
||||||
|
`setPctureInPictureAvailable` methods in the Jitsi Meet view or activity.
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,12 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
|
@ -95,7 +95,9 @@ public class MainActivity extends JitsiMeetActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
// As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do
|
// As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do
|
||||||
// want the Welcome page to be enabled. It defaults to disabled in the
|
// want to enable some options.
|
||||||
|
|
||||||
|
// The welcome page defaults to disabled in the
|
||||||
// SDK at the time of this writing but it is clearer to be explicit
|
// SDK at the time of this writing but it is clearer to be explicit
|
||||||
// about what we want anyway.
|
// about what we want anyway.
|
||||||
setWelcomePageEnabled(true);
|
setWelcomePageEnabled(true);
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.jitsi.meet.sdk;
|
package org.jitsi.meet.sdk;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -41,9 +42,7 @@ import java.net.URL;
|
||||||
* hooked to the React Native subsystem via proxy calls through the
|
* hooked to the React Native subsystem via proxy calls through the
|
||||||
* {@code JKConferenceView} static methods.
|
* {@code JKConferenceView} static methods.
|
||||||
*/
|
*/
|
||||||
public class JitsiMeetActivity
|
public class JitsiMeetActivity extends AppCompatActivity {
|
||||||
extends AppCompatActivity {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request code identifying requests for the permission to draw on top
|
* The request code identifying requests for the permission to draw on top
|
||||||
* of other apps. The value must be 16-bit and is arbitrarily chosen here.
|
* of other apps. The value must be 16-bit and is arbitrarily chosen here.
|
||||||
|
@ -69,6 +68,12 @@ public class JitsiMeetActivity
|
||||||
*/
|
*/
|
||||||
private JitsiMeetView view;
|
private JitsiMeetView view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Picture-in-Picture is available. The value is used only while
|
||||||
|
* {@link #view} equals {@code null}.
|
||||||
|
*/
|
||||||
|
private Boolean pipAvailable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the Welcome page is enabled. The value is used only while
|
* Whether the Welcome page is enabled. The value is used only while
|
||||||
* {@link #view} equals {@code null}.
|
* {@link #view} equals {@code null}.
|
||||||
|
@ -91,6 +96,15 @@ public class JitsiMeetActivity
|
||||||
return view == null ? defaultURL : view.getDefaultURL();
|
return view == null ? defaultURL : view.getDefaultURL();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @see JitsiMeetView#getPictureInPictureAvailable()
|
||||||
|
*/
|
||||||
|
public Boolean getPictureInPictureAvailable() {
|
||||||
|
return view == null
|
||||||
|
? pipAvailable : view.getPictureInPictureAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @see JitsiMeetView#getWelcomePageEnabled()
|
* @see JitsiMeetView#getWelcomePageEnabled()
|
||||||
|
@ -123,6 +137,7 @@ public class JitsiMeetActivity
|
||||||
// XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
|
// XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
|
||||||
// is documented to need such an order in order to take effect:
|
// is documented to need such an order in order to take effect:
|
||||||
view.setDefaultURL(defaultURL);
|
view.setDefaultURL(defaultURL);
|
||||||
|
view.setPictureInPictureAvailable(pipAvailable);
|
||||||
view.setWelcomePageEnabled(welcomePageEnabled);
|
view.setWelcomePageEnabled(welcomePageEnabled);
|
||||||
|
|
||||||
view.loadURL(null);
|
view.loadURL(null);
|
||||||
|
@ -224,19 +239,24 @@ public class JitsiMeetActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onResume() {
|
||||||
super.onPause();
|
super.onResume();
|
||||||
|
|
||||||
|
defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
|
||||||
|
JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
JitsiMeetView.onHostPause(this);
|
JitsiMeetView.onHostPause(this);
|
||||||
defaultBackButtonImpl = null;
|
defaultBackButtonImpl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onUserLeaveHint() {
|
||||||
super.onResume();
|
JitsiMeetView.onUserLeaveHint();
|
||||||
|
|
||||||
defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
|
|
||||||
JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -251,6 +271,18 @@ public class JitsiMeetActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @see JitsiMeetView#setPictureInPictureAvailable(Boolean)
|
||||||
|
*/
|
||||||
|
public void setPictureInPictureAvailable(Boolean pipAvailable) {
|
||||||
|
if (view == null) {
|
||||||
|
this.pipAvailable = pipAvailable;
|
||||||
|
} else {
|
||||||
|
view.setPictureInPictureAvailable(pipAvailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @see JitsiMeetView#setWelcomePageEnabled(boolean)
|
* @see JitsiMeetView#setWelcomePageEnabled(boolean)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
@ -30,8 +31,11 @@ import com.facebook.react.ReactInstanceManager;
|
||||||
import com.facebook.react.ReactRootView;
|
import com.facebook.react.ReactRootView;
|
||||||
import com.facebook.react.bridge.NativeModule;
|
import com.facebook.react.bridge.NativeModule;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContext;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
import com.facebook.react.common.LifecycleState;
|
import com.facebook.react.common.LifecycleState;
|
||||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||||
import com.rnimmersive.RNImmersiveModule;
|
import com.rnimmersive.RNImmersiveModule;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
@ -65,6 +69,7 @@ public class JitsiMeetView extends FrameLayout {
|
||||||
new AppInfoModule(reactContext),
|
new AppInfoModule(reactContext),
|
||||||
new AudioModeModule(reactContext),
|
new AudioModeModule(reactContext),
|
||||||
new ExternalAPIModule(reactContext),
|
new ExternalAPIModule(reactContext),
|
||||||
|
new PictureInPictureModule(reactContext),
|
||||||
new ProximityModule(reactContext),
|
new ProximityModule(reactContext),
|
||||||
new WiFiStatsModule(reactContext)
|
new WiFiStatsModule(reactContext)
|
||||||
);
|
);
|
||||||
|
@ -243,6 +248,36 @@ public class JitsiMeetView extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity lifecycle method which should be called from
|
||||||
|
* {@code Activity.onUserLeaveHint} so we can do the required internal
|
||||||
|
* processing.
|
||||||
|
*
|
||||||
|
* This is currently not mandatory.
|
||||||
|
*/
|
||||||
|
public static void onUserLeaveHint() {
|
||||||
|
sendEvent("onUserLeaveHint", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to send an event to JavaScript.
|
||||||
|
*
|
||||||
|
* @param eventName {@code String} containing the event name.
|
||||||
|
* @param params {@code WritableMap} optional ancillary data for the event.
|
||||||
|
*/
|
||||||
|
private static void sendEvent(
|
||||||
|
String eventName, @Nullable WritableMap params) {
|
||||||
|
if (reactInstanceManager != null) {
|
||||||
|
ReactContext reactContext
|
||||||
|
= reactInstanceManager.getCurrentReactContext();
|
||||||
|
if (reactContext != null) {
|
||||||
|
reactContext
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit(eventName, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default base {@code URL} used to join a conference when a partial URL
|
* The default base {@code URL} used to join a conference when a partial URL
|
||||||
* (e.g. a room name only) is specified to {@link #loadURLString(String)} or
|
* (e.g. a room name only) is specified to {@link #loadURLString(String)} or
|
||||||
|
@ -264,6 +299,12 @@ public class JitsiMeetView extends FrameLayout {
|
||||||
*/
|
*/
|
||||||
private JitsiMeetViewListener listener;
|
private JitsiMeetViewListener listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Picture-in-Picture is available. If {@code null} it will default
|
||||||
|
* to {@code true} iff the platform supports it.
|
||||||
|
*/
|
||||||
|
private Boolean pipAvailable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React Native root view.
|
* React Native root view.
|
||||||
*/
|
*/
|
||||||
|
@ -328,6 +369,17 @@ public class JitsiMeetView extends FrameLayout {
|
||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether Picture-in-Picture is currently available. It's only
|
||||||
|
* supported on Android API >= 26 (Oreo), so it should not be enabled on
|
||||||
|
* older platform versions.
|
||||||
|
*
|
||||||
|
* @return {@code true} if PiP is available, {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public Boolean getPictureInPictureAvailable() {
|
||||||
|
return pipAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets whether the Welcome page is enabled. If {@code true}, the Welcome
|
* Gets whether the Welcome page is enabled. If {@code true}, the Welcome
|
||||||
* page is rendered when this {@code JitsiMeetView} is not at a URL
|
* page is rendered when this {@code JitsiMeetView} is not at a URL
|
||||||
|
@ -369,12 +421,25 @@ public class JitsiMeetView extends FrameLayout {
|
||||||
if (defaultURL != null) {
|
if (defaultURL != null) {
|
||||||
props.putString("defaultURL", defaultURL.toString());
|
props.putString("defaultURL", defaultURL.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// externalAPIScope
|
// externalAPIScope
|
||||||
props.putString("externalAPIScope", externalAPIScope);
|
props.putString("externalAPIScope", externalAPIScope);
|
||||||
|
|
||||||
|
// pipAvailable
|
||||||
|
boolean pipAvailable_;
|
||||||
|
if (pipAvailable == null) {
|
||||||
|
// set it based on platform availability
|
||||||
|
pipAvailable_ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||||
|
} else {
|
||||||
|
pipAvailable_ = pipAvailable.booleanValue();
|
||||||
|
}
|
||||||
|
props.putBoolean("pipAvailable", pipAvailable_);
|
||||||
|
|
||||||
// url
|
// url
|
||||||
if (urlObject != null) {
|
if (urlObject != null) {
|
||||||
props.putBundle("url", urlObject);
|
props.putBundle("url", urlObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
// welcomePageEnabled
|
// welcomePageEnabled
|
||||||
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
|
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
|
||||||
|
|
||||||
|
@ -462,6 +527,16 @@ public class JitsiMeetView extends FrameLayout {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether Picture-in-Picture is currently available.
|
||||||
|
*
|
||||||
|
* @param pipAvailable {@code true} if PiP is available, {@code false}
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public void setPictureInPictureAvailable(Boolean pipAvailable) {
|
||||||
|
this.pipAvailable = pipAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether the Welcome page is enabled. Must be called before
|
* Sets whether the Welcome page is enabled. Must be called before
|
||||||
* {@link #loadURL(URL)} for it to take effect.
|
* {@link #loadURL(URL)} for it to take effect.
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package org.jitsi.meet.sdk;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.PictureInPictureParams;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.Rational;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Promise;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
|
||||||
|
public class PictureInPictureModule extends ReactContextBaseJavaModule {
|
||||||
|
private final static String TAG = "PictureInPicture";
|
||||||
|
|
||||||
|
public PictureInPictureModule(ReactApplicationContext reactContext) {
|
||||||
|
super(reactContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enters Picture-in-Picture mode for the current activity. This is only
|
||||||
|
* supported in Android API >= 26.
|
||||||
|
*
|
||||||
|
* @param promise a {@code Promise} which will resolve with a {@code null}
|
||||||
|
* value in case of success, and an error otherwise.
|
||||||
|
*/
|
||||||
|
@ReactMethod
|
||||||
|
public void enterPictureInPictureMode(Promise promise) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
final Activity currentActivity = getCurrentActivity();
|
||||||
|
|
||||||
|
if (currentActivity == null) {
|
||||||
|
promise.reject(new Exception("No current Activity!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Entering PiP mode");
|
||||||
|
|
||||||
|
final PictureInPictureParams.Builder pipParamsBuilder
|
||||||
|
= new PictureInPictureParams.Builder();
|
||||||
|
pipParamsBuilder.setAspectRatio(new Rational(1, 1)).build();
|
||||||
|
final boolean r
|
||||||
|
= currentActivity.enterPictureInPictureMode(pipParamsBuilder.build());
|
||||||
|
if (r) {
|
||||||
|
promise.resolve(null);
|
||||||
|
} else {
|
||||||
|
promise.reject(new Exception("Error entering PiP mode"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.reject(new Exception("PiP not supported"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,13 @@ built in JavaScript is used: https://meet.jit.si.
|
||||||
|
|
||||||
NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
|
NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
|
||||||
|
|
||||||
|
#### pipAvailable
|
||||||
|
|
||||||
|
Property to get / set wether a Picture-in-Picture mode is available. This must
|
||||||
|
be implemented by the application at the moment.
|
||||||
|
|
||||||
|
NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
|
||||||
|
|
||||||
#### welcomePageEnabled
|
#### welcomePageEnabled
|
||||||
|
|
||||||
Property to get/set whether the Welcome page is enabled. If `NO`, a black empty
|
Property to get/set whether the Welcome page is enabled. If `NO`, a black empty
|
||||||
|
@ -178,3 +185,24 @@ fails.
|
||||||
The `data` dictionary contains an "error" key with the error and a "url" key
|
The `data` dictionary contains an "error" key with the error and a "url" key
|
||||||
with the conference URL which necessitated the loading of the configuration
|
with the conference URL which necessitated the loading of the configuration
|
||||||
file.
|
file.
|
||||||
|
|
||||||
|
#### requestPipMode
|
||||||
|
|
||||||
|
Called when the user requested Picture-in-Picture mode to be entered. At this
|
||||||
|
point the application should resize the SDK view to a smaller size if it so
|
||||||
|
desires.
|
||||||
|
|
||||||
|
### Picture-in-Picture
|
||||||
|
|
||||||
|
The Jitsi Meet SDK implements a "reduced UI mode" which will automatically
|
||||||
|
adjust the UI when presented in a Picture-in-Picture style scenario. Enabling
|
||||||
|
a native Picture-in-Picture mode on iOS is not currently implemented on the SDK
|
||||||
|
so applications need to do it themselves.
|
||||||
|
|
||||||
|
When `pipAvailable` is set to `YES` or the `requestPipMode` delegate method is
|
||||||
|
implemented, the in-call toolbar will show a button to enter PiP mode. It's up
|
||||||
|
to the application to reduce the size of the SDK view and put it in such mode.
|
||||||
|
|
||||||
|
Once PiP mode has been entered, the SDK will automatically adjust its UI
|
||||||
|
elements.
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
|
|
||||||
@property (copy, nonatomic, nullable) NSURL *defaultURL;
|
@property (copy, nonatomic, nullable) NSURL *defaultURL;
|
||||||
|
|
||||||
|
@property (nonatomic) BOOL pipAvailable;
|
||||||
|
|
||||||
@property (nonatomic) BOOL welcomePageEnabled;
|
@property (nonatomic) BOOL welcomePageEnabled;
|
||||||
|
|
||||||
+ (BOOL)application:(UIApplication *_Nonnull)application
|
+ (BOOL)application:(UIApplication *_Nonnull)application
|
||||||
|
|
|
@ -109,7 +109,11 @@ void registerFatalErrorHandler() {
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation JitsiMeetView
|
@implementation JitsiMeetView {
|
||||||
|
NSNumber *_pipAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@dynamic pipAvailable;
|
||||||
|
|
||||||
static RCTBridgeWrapper *bridgeWrapper;
|
static RCTBridgeWrapper *bridgeWrapper;
|
||||||
|
|
||||||
|
@ -265,6 +269,7 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
|
||||||
}
|
}
|
||||||
|
|
||||||
props[@"externalAPIScope"] = externalAPIScope;
|
props[@"externalAPIScope"] = externalAPIScope;
|
||||||
|
props[@"pipAvailable"] = @(self.pipAvailable);
|
||||||
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
|
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
|
||||||
|
|
||||||
// XXX If urlObject is nil, then it must appear as undefined in the
|
// XXX If urlObject is nil, then it must appear as undefined in the
|
||||||
|
@ -315,6 +320,21 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
|
||||||
[self loadURLObject:urlString ? @{ @"url": urlString } : nil];
|
[self loadURLObject:urlString ? @{ @"url": urlString } : nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma pipAvailable getter / setter
|
||||||
|
|
||||||
|
- (void) setPipAvailable:(BOOL)pipAvailable {
|
||||||
|
_pipAvailable = [NSNumber numberWithBool:pipAvailable];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL) pipAvailable {
|
||||||
|
if (_pipAvailable == nil) {
|
||||||
|
return self.delegate
|
||||||
|
&& [self.delegate respondsToSelector:@selector(requestPipMode:)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [_pipAvailable boolValue];
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark Private methods
|
#pragma mark Private methods
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -65,4 +65,13 @@
|
||||||
*/
|
*/
|
||||||
- (void)loadConfigError:(NSDictionary *)data;
|
- (void)loadConfigError:(NSDictionary *)data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when Picture-in-Picture mode is requested. The app should now resize
|
||||||
|
* iself to a PiP style and then use the JitsiMeetView.onPipModeChanged to
|
||||||
|
* notify the JavaScript side about its action.
|
||||||
|
*
|
||||||
|
* The `data` dictionary is currently empty.
|
||||||
|
*/
|
||||||
|
- (void)requestPipMode:(NSDictionary *)data;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -17,6 +17,7 @@ import '../../mobile/callkit';
|
||||||
import '../../mobile/external-api';
|
import '../../mobile/external-api';
|
||||||
import '../../mobile/full-screen';
|
import '../../mobile/full-screen';
|
||||||
import '../../mobile/permissions';
|
import '../../mobile/permissions';
|
||||||
|
import '../../mobile/picture-in-picture';
|
||||||
import '../../mobile/proximity';
|
import '../../mobile/proximity';
|
||||||
import '../../mobile/wake-lock';
|
import '../../mobile/wake-lock';
|
||||||
|
|
||||||
|
@ -36,6 +37,12 @@ export class App extends AbstractApp {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
...AbstractApp.propTypes,
|
...AbstractApp.propTypes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Picture-in-Picture is available. If available, a button will
|
||||||
|
* be shown in the {@link Conference} view so the user can enter it.
|
||||||
|
*/
|
||||||
|
pipAvailable: PropTypes.bool,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the Welcome page is enabled. If {@code true}, the Welcome
|
* Whether the Welcome page is enabled. If {@code true}, the Welcome
|
||||||
* page is rendered when the {@link App} is not at a location (URL)
|
* page is rendered when the {@link App} is not at a location (URL)
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { LOAD_CONFIG_ERROR } from '../../base/config';
|
||||||
import { MiddlewareRegistry } from '../../base/redux';
|
import { MiddlewareRegistry } from '../../base/redux';
|
||||||
import { toURLString } from '../../base/util';
|
import { toURLString } from '../../base/util';
|
||||||
|
|
||||||
|
import { REQUEST_PIP_MODE } from '../picture-in-picture';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware that captures Redux actions and uses the ExternalAPI module to
|
* Middleware that captures Redux actions and uses the ExternalAPI module to
|
||||||
* turn them into native events so the application knows about them.
|
* turn them into native events so the application knows about them.
|
||||||
|
@ -62,6 +64,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case REQUEST_PIP_MODE:
|
||||||
|
_sendEvent(store, _getSymbolDescription(action.type), /* data */ {});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* The type of redux action to set the PiP related event listeners.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: _SET_PIP_MODE_LISTENER,
|
||||||
|
* listeners: Array|undefined
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
export const _SET_PIP_LISTENERS = Symbol('_SET_PIP_LISTENERS');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of redux action which signals that the PiP mode is requested.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: REQUEST_PIP_MODE
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const REQUEST_PIP_MODE = Symbol('REQUEST_PIP_MODE');
|
|
@ -0,0 +1,37 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
_SET_PIP_LISTENERS,
|
||||||
|
REQUEST_PIP_MODE
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the listeners for the PiP related events.
|
||||||
|
*
|
||||||
|
* @param {Array} listeners - Array of listeners to be set.
|
||||||
|
* @protected
|
||||||
|
* @returns {{
|
||||||
|
* type: _SET_PIP_LISTENERS,
|
||||||
|
* listeners: Array
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function _setListeners(listeners: ?Array<any>) {
|
||||||
|
return {
|
||||||
|
type: _SET_PIP_LISTENERS,
|
||||||
|
listeners
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests Picture-in-Picture mode.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @returns {{
|
||||||
|
* type: REQUEST_PIP_MODE
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function requestPipMode() {
|
||||||
|
return {
|
||||||
|
type: REQUEST_PIP_MODE
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { NativeModules } from 'react-native';
|
||||||
|
|
||||||
|
const pip = NativeModules.PictureInPicture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the application to enter the Picture-in-Picture mode, if supported.
|
||||||
|
*
|
||||||
|
* @returns {Promise} A promise which is fulfilled when PiP mode was entered, or
|
||||||
|
* rejected in case there was a problem or it isn't supported.
|
||||||
|
*/
|
||||||
|
export function enterPictureInPictureMode(): Promise<void> {
|
||||||
|
if (pip) {
|
||||||
|
return pip.enterPictureInPictureMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error('PiP not supported'));
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './actions';
|
||||||
|
export * from './actionTypes';
|
||||||
|
export * from './functions';
|
||||||
|
|
||||||
|
import './middleware';
|
||||||
|
import './reducer';
|
|
@ -0,0 +1,100 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { DeviceEventEmitter } from 'react-native';
|
||||||
|
|
||||||
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
|
||||||
|
import { MiddlewareRegistry } from '../../base/redux';
|
||||||
|
|
||||||
|
import { _setListeners } from './actions';
|
||||||
|
import { _SET_PIP_LISTENERS, REQUEST_PIP_MODE } from './actionTypes';
|
||||||
|
import { enterPictureInPictureMode } from './functions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware that handles Picture-in-Picture requests. Currently it enters
|
||||||
|
* the native PiP mode on Android, when requested.
|
||||||
|
*
|
||||||
|
* @param {Store} store - Redux store.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
switch (action.type) {
|
||||||
|
case _SET_PIP_LISTENERS: {
|
||||||
|
// Remove the current/old listeners.
|
||||||
|
const { listeners } = store.getState()['features/pip'];
|
||||||
|
|
||||||
|
if (listeners) {
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case APP_WILL_MOUNT:
|
||||||
|
_appWillMount(store);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case APP_WILL_UNMOUNT:
|
||||||
|
store.dispatch(_setListeners(undefined));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REQUEST_PIP_MODE:
|
||||||
|
_enterPictureInPicture(store);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the feature pip that the action {@link APP_WILL_MOUNT} is being
|
||||||
|
* dispatched within a specific redux {@code store}.
|
||||||
|
*
|
||||||
|
* @param {Store} store - The redux store in which the specified {@code action}
|
||||||
|
* is being dispatched.
|
||||||
|
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||||
|
* specified {@code action} to the specified {@code store}.
|
||||||
|
* @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
|
||||||
|
* being dispatched in the specified {@code store}.
|
||||||
|
* @private
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function _appWillMount({ dispatch, getState }) {
|
||||||
|
const context = {
|
||||||
|
dispatch,
|
||||||
|
getState
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners = [
|
||||||
|
|
||||||
|
// Android's onUserLeaveHint activity lifecycle callback
|
||||||
|
DeviceEventEmitter.addListener('onUserLeaveHint', () => {
|
||||||
|
_enterPictureInPicture(context);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
dispatch(_setListeners(listeners));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to enter PiP mode. This is triggered by user request
|
||||||
|
* (either pressing the button in the toolbox or the home button on Android)
|
||||||
|
* ans this triggers the PiP mode, iff it's available and we are in a
|
||||||
|
* conference.
|
||||||
|
*
|
||||||
|
* @param {Object} store - Redux store.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _enterPictureInPicture({ getState }) {
|
||||||
|
const state = getState();
|
||||||
|
const { app } = state['features/app'];
|
||||||
|
const { conference, joining } = state['features/base/conference'];
|
||||||
|
|
||||||
|
if (app.props.pipAvailable && (conference || joining)) {
|
||||||
|
enterPictureInPictureMode().catch(e => {
|
||||||
|
console.warn(`Error entering PiP mode: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ReducerRegistry } from '../../base/redux';
|
||||||
|
|
||||||
|
import { _SET_PIP_LISTENERS } from './actionTypes';
|
||||||
|
|
||||||
|
ReducerRegistry.register('features/pip', (state = {}, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case _SET_PIP_LISTENERS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
listeners: action.listeners
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
|
@ -23,6 +23,7 @@ import {
|
||||||
makeAspectRatioAware
|
makeAspectRatioAware
|
||||||
} from '../../base/responsive-ui';
|
} from '../../base/responsive-ui';
|
||||||
import { ColorPalette } from '../../base/styles';
|
import { ColorPalette } from '../../base/styles';
|
||||||
|
import { requestPipMode } from '../../mobile/picture-in-picture';
|
||||||
import { beginRoomLockRequest } from '../../room-lock';
|
import { beginRoomLockRequest } from '../../room-lock';
|
||||||
import { beginShareRoom } from '../../share-room';
|
import { beginShareRoom } from '../../share-room';
|
||||||
|
|
||||||
|
@ -80,6 +81,11 @@ class Toolbox extends Component {
|
||||||
*/
|
*/
|
||||||
_onHangup: PropTypes.func,
|
_onHangup: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests Picture-in-Picture mode.
|
||||||
|
*/
|
||||||
|
_onPipRequest: PropTypes.func,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the lock i.e. password protection of the conference/room.
|
* Sets the lock i.e. password protection of the conference/room.
|
||||||
*/
|
*/
|
||||||
|
@ -101,6 +107,11 @@ class Toolbox extends Component {
|
||||||
*/
|
*/
|
||||||
_onToggleCameraFacingMode: PropTypes.func,
|
_onToggleCameraFacingMode: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag showing whether Picture-in-Picture is available.
|
||||||
|
*/
|
||||||
|
_pipAvailable: PropTypes.bool,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag showing whether video is muted.
|
* Flag showing whether video is muted.
|
||||||
*/
|
*/
|
||||||
|
@ -296,6 +307,7 @@ class Toolbox extends Component {
|
||||||
const underlayColor = 'transparent';
|
const underlayColor = 'transparent';
|
||||||
const {
|
const {
|
||||||
_audioOnly: audioOnly,
|
_audioOnly: audioOnly,
|
||||||
|
_pipAvailable: pipAvailable,
|
||||||
_videoMuted: videoMuted
|
_videoMuted: videoMuted
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -305,6 +317,15 @@ class Toolbox extends Component {
|
||||||
<View
|
<View
|
||||||
key = 'secondaryToolbar'
|
key = 'secondaryToolbar'
|
||||||
style = { styles.secondaryToolbar }>
|
style = { styles.secondaryToolbar }>
|
||||||
|
{
|
||||||
|
pipAvailable
|
||||||
|
&& <ToolbarButton
|
||||||
|
iconName = { 'menu-down' }
|
||||||
|
iconStyle = { iconStyle }
|
||||||
|
onClick = { this.props._onPipRequest }
|
||||||
|
style = { style }
|
||||||
|
underlayColor = { underlayColor } />
|
||||||
|
}
|
||||||
{
|
{
|
||||||
AudioRouteButton
|
AudioRouteButton
|
||||||
&& <AudioRouteButton
|
&& <AudioRouteButton
|
||||||
|
@ -391,6 +412,17 @@ function _mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
...abstractMapDispatchToProps(dispatch),
|
...abstractMapDispatchToProps(dispatch),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests Picture-in-Picture mode.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
_onPipRequest() {
|
||||||
|
dispatch(requestPipMode());
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the lock i.e. password protection of the conference/room.
|
* Sets the lock i.e. password protection of the conference/room.
|
||||||
*
|
*
|
||||||
|
@ -451,6 +483,7 @@ function _mapDispatchToProps(dispatch) {
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
const conference = state['features/base/conference'];
|
const conference = state['features/base/conference'];
|
||||||
const { enabled } = state['features/toolbox'];
|
const { enabled } = state['features/toolbox'];
|
||||||
|
const { app } = state['features/app'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...abstractMapStateToProps(state),
|
...abstractMapStateToProps(state),
|
||||||
|
@ -479,7 +512,15 @@ function _mapStateToProps(state) {
|
||||||
* @protected
|
* @protected
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
_locked: Boolean(conference.locked)
|
_locked: Boolean(conference.locked),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The indicator which determines if Picture-in-Picture is available.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
_pipAvailable: Boolean(app && app.props.pipAvailable)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue