Browse Source

[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.
master
Saúl Ibarra Corretgé 7 years ago
parent
commit
b3683068d4

+ 44
- 3
android/README.md View File

@@ -118,8 +118,8 @@ public class MainActivity extends AppCompatActivity {
118 118
     }
119 119
 
120 120
     @Override
121
-    protected void onPause() {
122
-        super.onPause();
121
+    protected void onStop() {
122
+        super.onStop();
123 123
 
124 124
         JitsiMeetView.onHostPause(this);
125 125
     }
@@ -142,6 +142,10 @@ which displays a single `JitsiMeetView`.
142 142
 
143 143
 See JitsiMeetView.getDefaultURL.
144 144
 
145
+#### getPictureInPictureAvailable()
146
+
147
+See JitsiMeetView.getPictureInPictureAvailable.
148
+
145 149
 #### getWelcomePageEnabled()
146 150
 
147 151
 See JitsiMeetView.getWelcomePageEnabled.
@@ -154,6 +158,10 @@ See JitsiMeetView.loadURL.
154 158
 
155 159
 See JitsiMeetView.setDefaultURL.
156 160
 
161
+#### setPictureInPictureAvailable(Boolean)
162
+
163
+See JitsiMeetView.setPictureInPictureAvailable.
164
+
157 165
 #### setWelcomePageEnabled(boolean)
158 166
 
159 167
 See JitsiMeetView.setWelcomePageEnabled.
@@ -179,6 +187,12 @@ if set to `null`, the default built in JavaScript is used: https://meet.jit.si.
179 187
 
180 188
 Returns the `JitsiMeetViewListener` instance attached to the view.
181 189
 
190
+#### getPictureInPictureAvailable()
191
+
192
+turns true if Picture-in-Picture is available, false otherwise. If the user
193
+doesn't explicitly set it, it will default to true if the platform supports it,
194
+false otherwise. See the Picture-in-Picture section.
195
+
182 196
 #### getWelcomePageEnabled()
183 197
 
184 198
 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.
227 241
 Sets the given listener (class implementing the `JitsiMeetViewListener`
228 242
 interface) on the view.
229 243
 
244
+#### setPictureInPictureAvailable(Boolean)
245
+
246
+Sets wether Picture-in-Picture is available. When set to `null` if will be
247
+detected at runtime based on platform support.
248
+
249
+NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
250
+
230 251
 #### setWelcomePageEnabled(boolean)
231 252
 
232 253
 Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
@@ -257,7 +278,8 @@ This is a static method.
257 278
 
258 279
 #### onHostResume(activity)
259 280
 
260
-Helper method which should be called from the activity's `onResume` method.
281
+Helper method which should be called from the activity's `onResume` or `onStop`
282
+method.
261 283
 
262 284
 This is a static method.
263 285
 
@@ -269,6 +291,13 @@ activity's `onNewIntent` method.
269 291
 
270 292
 This is a static method.
271 293
 
294
+#### onUserLeaveHint()
295
+
296
+Helper method for integrating automatic Picture-in-Picture. It should be called
297
+from the activity's `onUserLeaveHint` method.
298
+
299
+This is a static method.
300
+
272 301
 #### JitsiMeetViewListener
273 302
 
274 303
 `JitsiMeetViewListener` provides an interface apps can implement to listen to
@@ -385,3 +414,15 @@ rules file:
385 414
 -keep class org.jitsi.meet.sdk.** { *; }
386 415
 ```
387 416
 
417
+## Picture-in-Picture
418
+
419
+The Jitsi Meet app and SDK will enable Android's native Picture-in-Picture mode
420
+iff the platform is supported: for Android >= Oreo.
421
+
422
+If the SDK is integrated in an application which calls
423
+`enterPictureInPictureMode` for the Jitsi Meet activity, the it will self-adjust
424
+by removing some UI elements.
425
+
426
+Alternatively, this can be explicitly disabled with the
427
+`setPctureInPictureAvailable` methods in the Jitsi Meet view or activity.
428
+

+ 3
- 1
android/app/src/main/AndroidManifest.xml View File

@@ -7,10 +7,12 @@
7 7
       android:label="@string/app_name"
8 8
       android:theme="@style/AppTheme">
9 9
     <activity
10
-        android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
10
+        android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
11 11
         android:label="@string/app_name"
12 12
         android:launchMode="singleTask"
13 13
         android:name=".MainActivity"
14
+        android:resizeableActivity="true"
15
+        android:supportsPictureInPicture="true"
14 16
         android:windowSoftInputMode="adjustResize">
15 17
       <intent-filter>
16 18
         <action android:name="android.intent.action.MAIN" />

+ 3
- 1
android/app/src/main/java/org/jitsi/meet/MainActivity.java View File

@@ -95,7 +95,9 @@ public class MainActivity extends JitsiMeetActivity {
95 95
     @Override
96 96
     protected void onCreate(Bundle savedInstanceState) {
97 97
         // As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do
98
-        // want the Welcome page to be enabled. It defaults to disabled in the
98
+        // want to enable some options.
99
+
100
+        // The welcome page defaults to disabled in the
99 101
         // SDK at the time of this writing but it is clearer to be explicit
100 102
         // about what we want anyway.
101 103
         setWelcomePageEnabled(true);

+ 42
- 10
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java View File

@@ -17,6 +17,7 @@
17 17
 package org.jitsi.meet.sdk;
18 18
 
19 19
 import android.content.Intent;
20
+import android.content.res.Configuration;
20 21
 import android.net.Uri;
21 22
 import android.os.Build;
22 23
 import android.os.Bundle;
@@ -41,9 +42,7 @@ import java.net.URL;
41 42
  * hooked to the React Native subsystem via proxy calls through the
42 43
  * {@code JKConferenceView} static methods.
43 44
  */
44
-public class JitsiMeetActivity
45
-    extends AppCompatActivity {
46
-
45
+public class JitsiMeetActivity extends AppCompatActivity {
47 46
     /**
48 47
      * The request code identifying requests for the permission to draw on top
49 48
      * of other apps. The value must be 16-bit and is arbitrarily chosen here.
@@ -69,6 +68,12 @@ public class JitsiMeetActivity
69 68
      */
70 69
     private JitsiMeetView view;
71 70
 
71
+    /**
72
+     * Whether Picture-in-Picture is available. The value is used only while
73
+     * {@link #view} equals {@code null}.
74
+     */
75
+    private Boolean pipAvailable;
76
+
72 77
     /**
73 78
      * Whether the Welcome page is enabled. The value is used only while
74 79
      * {@link #view} equals {@code null}.
@@ -91,6 +96,15 @@ public class JitsiMeetActivity
91 96
         return view == null ? defaultURL : view.getDefaultURL();
92 97
     }
93 98
 
99
+    /**
100
+     *
101
+     * @see JitsiMeetView#getPictureInPictureAvailable()
102
+     */
103
+    public Boolean getPictureInPictureAvailable() {
104
+        return view == null
105
+            ? pipAvailable : view.getPictureInPictureAvailable();
106
+    }
107
+
94 108
     /**
95 109
      *
96 110
      * @see JitsiMeetView#getWelcomePageEnabled()
@@ -123,6 +137,7 @@ public class JitsiMeetActivity
123 137
         // XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
124 138
         // is documented to need such an order in order to take effect:
125 139
         view.setDefaultURL(defaultURL);
140
+        view.setPictureInPictureAvailable(pipAvailable);
126 141
         view.setWelcomePageEnabled(welcomePageEnabled);
127 142
 
128 143
         view.loadURL(null);
@@ -224,19 +239,24 @@ public class JitsiMeetActivity
224 239
     }
225 240
 
226 241
     @Override
227
-    protected void onPause() {
228
-        super.onPause();
242
+    protected void onResume() {
243
+        super.onResume();
244
+
245
+        defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
246
+        JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
247
+    }
248
+
249
+    @Override
250
+    public void onStop() {
251
+        super.onStop();
229 252
 
230 253
         JitsiMeetView.onHostPause(this);
231 254
         defaultBackButtonImpl = null;
232 255
     }
233 256
 
234 257
     @Override
235
-    protected void onResume() {
236
-        super.onResume();
237
-
238
-        defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
239
-        JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
258
+    protected void onUserLeaveHint() {
259
+        JitsiMeetView.onUserLeaveHint();
240 260
     }
241 261
 
242 262
     /**
@@ -251,6 +271,18 @@ public class JitsiMeetActivity
251 271
         }
252 272
     }
253 273
 
274
+    /**
275
+     *
276
+     * @see JitsiMeetView#setPictureInPictureAvailable(Boolean)
277
+     */
278
+    public void setPictureInPictureAvailable(Boolean pipAvailable) {
279
+        if (view == null) {
280
+            this.pipAvailable = pipAvailable;
281
+        } else {
282
+            view.setPictureInPictureAvailable(pipAvailable);
283
+        }
284
+    }
285
+
254 286
     /**
255 287
      *
256 288
      * @see JitsiMeetView#setWelcomePageEnabled(boolean)

+ 75
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java View File

@@ -21,6 +21,7 @@ import android.app.Application;
21 21
 import android.content.Context;
22 22
 import android.content.Intent;
23 23
 import android.net.Uri;
24
+import android.os.Build;
24 25
 import android.os.Bundle;
25 26
 import android.support.annotation.NonNull;
26 27
 import android.support.annotation.Nullable;
@@ -30,8 +31,11 @@ import com.facebook.react.ReactInstanceManager;
30 31
 import com.facebook.react.ReactRootView;
31 32
 import com.facebook.react.bridge.NativeModule;
32 33
 import com.facebook.react.bridge.ReactApplicationContext;
34
+import com.facebook.react.bridge.ReactContext;
35
+import com.facebook.react.bridge.WritableMap;
33 36
 import com.facebook.react.common.LifecycleState;
34 37
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
38
+import com.facebook.react.modules.core.DeviceEventManagerModule;
35 39
 import com.rnimmersive.RNImmersiveModule;
36 40
 
37 41
 import java.net.URL;
@@ -65,6 +69,7 @@ public class JitsiMeetView extends FrameLayout {
65 69
             new AppInfoModule(reactContext),
66 70
             new AudioModeModule(reactContext),
67 71
             new ExternalAPIModule(reactContext),
72
+            new PictureInPictureModule(reactContext),
68 73
             new ProximityModule(reactContext),
69 74
             new WiFiStatsModule(reactContext)
70 75
         );
@@ -243,6 +248,36 @@ public class JitsiMeetView extends FrameLayout {
243 248
         }
244 249
     }
245 250
 
251
+    /**
252
+     * Activity lifecycle method which should be called from
253
+     * {@code Activity.onUserLeaveHint} so we can do the required internal
254
+     * processing.
255
+     *
256
+     * This is currently not mandatory.
257
+     */
258
+    public static void onUserLeaveHint() {
259
+        sendEvent("onUserLeaveHint", null);
260
+    }
261
+
262
+    /**
263
+     * Helper function to send an event to JavaScript.
264
+     *
265
+     * @param eventName {@code String} containing the event name.
266
+     * @param params {@code WritableMap} optional ancillary data for the event.
267
+     */
268
+    private static void sendEvent(
269
+            String eventName, @Nullable WritableMap params) {
270
+        if (reactInstanceManager != null) {
271
+            ReactContext reactContext
272
+                = reactInstanceManager.getCurrentReactContext();
273
+            if (reactContext != null) {
274
+                reactContext
275
+                    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
276
+                    .emit(eventName, params);
277
+            }
278
+        }
279
+    }
280
+
246 281
     /**
247 282
      * The default base {@code URL} used to join a conference when a partial URL
248 283
      * (e.g. a room name only) is specified to {@link #loadURLString(String)} or
@@ -264,6 +299,12 @@ public class JitsiMeetView extends FrameLayout {
264 299
      */
265 300
     private JitsiMeetViewListener listener;
266 301
 
302
+    /**
303
+     * Whether Picture-in-Picture is available. If {@code null}  it will default
304
+     * to {@code true} iff the platform supports it.
305
+     */
306
+    private Boolean pipAvailable;
307
+
267 308
     /**
268 309
      * React Native root view.
269 310
      */
@@ -328,6 +369,17 @@ public class JitsiMeetView extends FrameLayout {
328 369
         return listener;
329 370
     }
330 371
 
372
+    /**
373
+     * Gets whether Picture-in-Picture is currently available. It's only
374
+     * supported on Android API >= 26 (Oreo), so it should not be enabled on
375
+     * older platform versions.
376
+     *
377
+     * @return {@code true} if PiP is available, {@code false} otherwise.
378
+     */
379
+    public Boolean getPictureInPictureAvailable() {
380
+        return pipAvailable;
381
+    }
382
+
331 383
     /**
332 384
      * Gets whether the Welcome page is enabled. If {@code true}, the Welcome
333 385
      * page is rendered when this {@code JitsiMeetView} is not at a URL
@@ -369,12 +421,25 @@ public class JitsiMeetView extends FrameLayout {
369 421
         if (defaultURL != null) {
370 422
             props.putString("defaultURL", defaultURL.toString());
371 423
         }
424
+
372 425
         // externalAPIScope
373 426
         props.putString("externalAPIScope", externalAPIScope);
427
+
428
+        // pipAvailable
429
+        boolean pipAvailable_;
430
+        if (pipAvailable == null) {
431
+            // set it based on platform availability
432
+            pipAvailable_ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
433
+        } else {
434
+            pipAvailable_ = pipAvailable.booleanValue();
435
+        }
436
+        props.putBoolean("pipAvailable", pipAvailable_);
437
+
374 438
         // url
375 439
         if (urlObject != null) {
376 440
             props.putBundle("url", urlObject);
377 441
         }
442
+
378 443
         // welcomePageEnabled
379 444
         props.putBoolean("welcomePageEnabled", welcomePageEnabled);
380 445
 
@@ -462,6 +527,16 @@ public class JitsiMeetView extends FrameLayout {
462 527
         this.listener = listener;
463 528
     }
464 529
 
530
+    /**
531
+     * Sets whether Picture-in-Picture is currently available.
532
+     *
533
+     * @param pipAvailable {@code true} if PiP is available, {@code false}
534
+     * otherwise.
535
+     */
536
+    public void setPictureInPictureAvailable(Boolean pipAvailable) {
537
+        this.pipAvailable = pipAvailable;
538
+    }
539
+
465 540
     /**
466 541
      * Sets whether the Welcome page is enabled. Must be called before
467 542
      * {@link #loadURL(URL)} for it to take effect.

+ 61
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java View File

@@ -0,0 +1,61 @@
1
+package org.jitsi.meet.sdk;
2
+
3
+import android.app.Activity;
4
+import android.app.PictureInPictureParams;
5
+import android.os.Build;
6
+import android.util.Log;
7
+import android.util.Rational;
8
+
9
+import com.facebook.react.bridge.Promise;
10
+import com.facebook.react.bridge.ReactApplicationContext;
11
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
12
+import com.facebook.react.bridge.ReactMethod;
13
+
14
+public class PictureInPictureModule extends ReactContextBaseJavaModule {
15
+    private final static String TAG = "PictureInPicture";
16
+
17
+    public PictureInPictureModule(ReactApplicationContext reactContext) {
18
+        super(reactContext);
19
+    }
20
+
21
+    @Override
22
+    public String getName() {
23
+        return TAG;
24
+    }
25
+
26
+    /**
27
+     * Enters Picture-in-Picture mode for the current activity. This is only
28
+     * supported in Android API >= 26.
29
+     *
30
+     * @param promise a {@code Promise} which will resolve with a {@code null}
31
+     *                value in case of success, and an error otherwise.
32
+     */
33
+    @ReactMethod
34
+    public void enterPictureInPictureMode(Promise promise) {
35
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
36
+            final Activity currentActivity = getCurrentActivity();
37
+
38
+            if (currentActivity == null) {
39
+                promise.reject(new Exception("No current Activity!"));
40
+                return;
41
+            }
42
+
43
+            Log.d(TAG, "Entering PiP mode");
44
+
45
+            final PictureInPictureParams.Builder pipParamsBuilder
46
+                = new PictureInPictureParams.Builder();
47
+            pipParamsBuilder.setAspectRatio(new Rational(1, 1)).build();
48
+            final boolean r
49
+                = currentActivity.enterPictureInPictureMode(pipParamsBuilder.build());
50
+            if (r) {
51
+                promise.resolve(null);
52
+            } else {
53
+                promise.reject(new Exception("Error entering PiP mode"));
54
+            }
55
+
56
+            return;
57
+        }
58
+
59
+        promise.reject(new Exception("PiP not supported"));
60
+    }
61
+}

+ 28
- 0
ios/README.md View File

@@ -55,6 +55,13 @@ built in JavaScript is used: https://meet.jit.si.
55 55
 
56 56
 NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
57 57
 
58
+#### pipAvailable
59
+
60
+Property to get / set wether a Picture-in-Picture mode is available. This must
61
+be implemented by the application at the moment.
62
+
63
+NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
64
+
58 65
 #### welcomePageEnabled
59 66
 
60 67
 Property to get/set whether the Welcome page is enabled. If `NO`, a black empty
@@ -178,3 +185,24 @@ fails.
178 185
 The `data` dictionary contains an "error" key with the error and a "url" key
179 186
 with the conference URL which necessitated the loading of the configuration
180 187
 file.
188
+
189
+#### requestPipMode
190
+
191
+Called when the user requested Picture-in-Picture mode to be entered. At this
192
+point the application should resize the SDK view to a smaller size if it so
193
+desires.
194
+
195
+### Picture-in-Picture
196
+
197
+The Jitsi Meet SDK implements a "reduced UI mode" which will automatically
198
+adjust the UI when presented in a Picture-in-Picture style scenario. Enabling
199
+a native Picture-in-Picture mode on iOS is not currently implemented on the SDK
200
+so applications need to do it themselves.
201
+
202
+When `pipAvailable` is set to `YES` or the `requestPipMode` delegate method is
203
+implemented, the in-call toolbar will show a button to enter PiP mode. It's up
204
+to the application to reduce the size of the SDK view and put it in such mode.
205
+
206
+Once PiP mode has been entered, the SDK will automatically adjust its UI
207
+elements.
208
+

+ 2
- 0
ios/sdk/src/JitsiMeetView.h View File

@@ -25,6 +25,8 @@
25 25
 
26 26
 @property (copy, nonatomic, nullable) NSURL *defaultURL;
27 27
 
28
+@property (nonatomic) BOOL pipAvailable;
29
+
28 30
 @property (nonatomic) BOOL welcomePageEnabled;
29 31
 
30 32
 +             (BOOL)application:(UIApplication *_Nonnull)application

+ 21
- 1
ios/sdk/src/JitsiMeetView.m View File

@@ -109,7 +109,11 @@ void registerFatalErrorHandler() {
109 109
 
110 110
 @end
111 111
 
112
-@implementation JitsiMeetView
112
+@implementation JitsiMeetView {
113
+    NSNumber *_pipAvailable;
114
+}
115
+
116
+@dynamic pipAvailable;
113 117
 
114 118
 static RCTBridgeWrapper *bridgeWrapper;
115 119
 
@@ -265,6 +269,7 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
265 269
     }
266 270
 
267 271
     props[@"externalAPIScope"] = externalAPIScope;
272
+    props[@"pipAvailable"] = @(self.pipAvailable);
268 273
     props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
269 274
 
270 275
     // XXX If urlObject is nil, then it must appear as undefined in the
@@ -315,6 +320,21 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
315 320
     [self loadURLObject:urlString ? @{ @"url": urlString } : nil];
316 321
 }
317 322
 
323
+#pragma pipAvailable getter / setter
324
+
325
+- (void) setPipAvailable:(BOOL)pipAvailable {
326
+    _pipAvailable = [NSNumber numberWithBool:pipAvailable];
327
+}
328
+
329
+- (BOOL) pipAvailable {
330
+    if (_pipAvailable == nil) {
331
+        return self.delegate
332
+            && [self.delegate respondsToSelector:@selector(requestPipMode:)];
333
+    }
334
+
335
+    return [_pipAvailable boolValue];
336
+}
337
+
318 338
 #pragma mark Private methods
319 339
 
320 340
 /**

+ 9
- 0
ios/sdk/src/JitsiMeetViewDelegate.h View File

@@ -65,4 +65,13 @@
65 65
  */
66 66
 - (void)loadConfigError:(NSDictionary *)data;
67 67
 
68
+/**
69
+ * Called when Picture-in-Picture mode is requested. The app should now resize
70
+ * iself to a PiP style and then use the JitsiMeetView.onPipModeChanged to
71
+ * notify the JavaScript side about its action.
72
+ *
73
+ * The `data` dictionary is currently empty.
74
+ */
75
+- (void)requestPipMode:(NSDictionary *)data;
76
+
68 77
 @end

+ 7
- 0
react/features/app/components/App.native.js View File

@@ -17,6 +17,7 @@ import '../../mobile/callkit';
17 17
 import '../../mobile/external-api';
18 18
 import '../../mobile/full-screen';
19 19
 import '../../mobile/permissions';
20
+import '../../mobile/picture-in-picture';
20 21
 import '../../mobile/proximity';
21 22
 import '../../mobile/wake-lock';
22 23
 
@@ -36,6 +37,12 @@ export class App extends AbstractApp {
36 37
     static propTypes = {
37 38
         ...AbstractApp.propTypes,
38 39
 
40
+        /**
41
+         * Whether Picture-in-Picture is available. If available, a button will
42
+         * be shown in the {@link Conference} view so the user can enter it.
43
+         */
44
+        pipAvailable: PropTypes.bool,
45
+
39 46
         /**
40 47
          * Whether the Welcome page is enabled. If {@code true}, the Welcome
41 48
          * page is rendered when the {@link App} is not at a location (URL)

+ 6
- 0
react/features/mobile/external-api/middleware.js View File

@@ -14,6 +14,8 @@ import { LOAD_CONFIG_ERROR } from '../../base/config';
14 14
 import { MiddlewareRegistry } from '../../base/redux';
15 15
 import { toURLString } from '../../base/util';
16 16
 
17
+import { REQUEST_PIP_MODE } from '../picture-in-picture';
18
+
17 19
 /**
18 20
  * Middleware that captures Redux actions and uses the ExternalAPI module to
19 21
  * turn them into native events so the application knows about them.
@@ -62,6 +64,10 @@ MiddlewareRegistry.register(store => next => action => {
62 64
         });
63 65
         break;
64 66
     }
67
+
68
+    case REQUEST_PIP_MODE:
69
+        _sendEvent(store, _getSymbolDescription(action.type), /* data */ {});
70
+
65 71
     }
66 72
 
67 73
     return result;

+ 22
- 0
react/features/mobile/picture-in-picture/actionTypes.js View File

@@ -0,0 +1,22 @@
1
+/**
2
+ * The type of redux action to set the PiP related event listeners.
3
+ *
4
+ * {
5
+ *     type: _SET_PIP_MODE_LISTENER,
6
+ *     listeners: Array|undefined
7
+ * }
8
+ *
9
+ * @protected
10
+ */
11
+export const _SET_PIP_LISTENERS = Symbol('_SET_PIP_LISTENERS');
12
+
13
+/**
14
+ * The type of redux action which signals that the PiP mode is requested.
15
+ *
16
+ * {
17
+ *      type: REQUEST_PIP_MODE
18
+ * }
19
+ *
20
+ * @public
21
+ */
22
+export const REQUEST_PIP_MODE = Symbol('REQUEST_PIP_MODE');

+ 37
- 0
react/features/mobile/picture-in-picture/actions.js View File

@@ -0,0 +1,37 @@
1
+// @flow
2
+
3
+import {
4
+    _SET_PIP_LISTENERS,
5
+    REQUEST_PIP_MODE
6
+} from './actionTypes';
7
+
8
+/**
9
+ * Sets the listeners for the PiP related events.
10
+ *
11
+ * @param {Array} listeners - Array of listeners to be set.
12
+ * @protected
13
+ * @returns {{
14
+ *     type: _SET_PIP_LISTENERS,
15
+ *     listeners: Array
16
+ * }}
17
+ */
18
+export function _setListeners(listeners: ?Array<any>) {
19
+    return {
20
+        type: _SET_PIP_LISTENERS,
21
+        listeners
22
+    };
23
+}
24
+
25
+/**
26
+ * Requests Picture-in-Picture mode.
27
+ *
28
+ * @public
29
+ * @returns {{
30
+ *     type: REQUEST_PIP_MODE
31
+ * }}
32
+ */
33
+export function requestPipMode() {
34
+    return {
35
+        type: REQUEST_PIP_MODE
36
+    };
37
+}

+ 19
- 0
react/features/mobile/picture-in-picture/functions.js View File

@@ -0,0 +1,19 @@
1
+// @flow
2
+
3
+import { NativeModules } from 'react-native';
4
+
5
+const pip = NativeModules.PictureInPicture;
6
+
7
+/**
8
+ * Tells the application to enter the Picture-in-Picture mode, if supported.
9
+ *
10
+ * @returns {Promise} A promise which is fulfilled when PiP mode was entered, or
11
+ * rejected in case there was a problem or it isn't supported.
12
+ */
13
+export function enterPictureInPictureMode(): Promise<void> {
14
+    if (pip) {
15
+        return pip.enterPictureInPictureMode();
16
+    }
17
+
18
+    return Promise.reject(new Error('PiP not supported'));
19
+}

+ 6
- 0
react/features/mobile/picture-in-picture/index.js View File

@@ -0,0 +1,6 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+export * from './functions';
4
+
5
+import './middleware';
6
+import './reducer';

+ 100
- 0
react/features/mobile/picture-in-picture/middleware.js View File

@@ -0,0 +1,100 @@
1
+// @flow
2
+
3
+import { DeviceEventEmitter } from 'react-native';
4
+
5
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
6
+import { MiddlewareRegistry } from '../../base/redux';
7
+
8
+import { _setListeners } from './actions';
9
+import { _SET_PIP_LISTENERS, REQUEST_PIP_MODE } from './actionTypes';
10
+import { enterPictureInPictureMode } from './functions';
11
+
12
+/**
13
+ * Middleware that handles Picture-in-Picture requests. Currently it enters
14
+ * the native PiP mode on Android, when requested.
15
+ *
16
+ * @param {Store} store - Redux store.
17
+ * @returns {Function}
18
+ */
19
+MiddlewareRegistry.register(store => next => action => {
20
+    switch (action.type) {
21
+    case _SET_PIP_LISTENERS: {
22
+        // Remove the current/old listeners.
23
+        const { listeners } = store.getState()['features/pip'];
24
+
25
+        if (listeners) {
26
+            for (const listener of listeners) {
27
+                listener.remove();
28
+            }
29
+        }
30
+        break;
31
+    }
32
+
33
+    case APP_WILL_MOUNT:
34
+        _appWillMount(store);
35
+        break;
36
+
37
+    case APP_WILL_UNMOUNT:
38
+        store.dispatch(_setListeners(undefined));
39
+        break;
40
+
41
+    case REQUEST_PIP_MODE:
42
+        _enterPictureInPicture(store);
43
+        break;
44
+
45
+    }
46
+
47
+    return next(action);
48
+});
49
+
50
+/**
51
+ * Notifies the feature pip that the action {@link APP_WILL_MOUNT} is being
52
+ * dispatched within a specific redux {@code store}.
53
+ *
54
+ * @param {Store} store - The redux store in which the specified {@code action}
55
+ * is being dispatched.
56
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
57
+ * specified {@code action} to the specified {@code store}.
58
+ * @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
59
+ * being dispatched in the specified {@code store}.
60
+ * @private
61
+ * @returns {*}
62
+ */
63
+function _appWillMount({ dispatch, getState }) {
64
+    const context = {
65
+        dispatch,
66
+        getState
67
+    };
68
+
69
+    const listeners = [
70
+
71
+        // Android's onUserLeaveHint activity lifecycle callback
72
+        DeviceEventEmitter.addListener('onUserLeaveHint', () => {
73
+            _enterPictureInPicture(context);
74
+        })
75
+    ];
76
+
77
+    dispatch(_setListeners(listeners));
78
+}
79
+
80
+/**
81
+ * Helper function to enter PiP mode. This is triggered by user request
82
+ * (either pressing the button in the toolbox or the home button on Android)
83
+ * ans this triggers the PiP mode, iff it's available and we are in a
84
+ * conference.
85
+ *
86
+ * @param {Object} store - Redux store.
87
+ * @private
88
+ * @returns {void}
89
+ */
90
+function _enterPictureInPicture({ getState }) {
91
+    const state = getState();
92
+    const { app } = state['features/app'];
93
+    const { conference, joining } = state['features/base/conference'];
94
+
95
+    if (app.props.pipAvailable && (conference || joining)) {
96
+        enterPictureInPictureMode().catch(e => {
97
+            console.warn(`Error entering PiP mode: ${e}`);
98
+        });
99
+    }
100
+}

+ 15
- 0
react/features/mobile/picture-in-picture/reducer.js View File

@@ -0,0 +1,15 @@
1
+import { ReducerRegistry } from '../../base/redux';
2
+
3
+import { _SET_PIP_LISTENERS } from './actionTypes';
4
+
5
+ReducerRegistry.register('features/pip', (state = {}, action) => {
6
+    switch (action.type) {
7
+    case _SET_PIP_LISTENERS:
8
+        return {
9
+            ...state,
10
+            listeners: action.listeners
11
+        };
12
+    }
13
+
14
+    return state;
15
+});

+ 42
- 1
react/features/toolbox/components/Toolbox.native.js View File

@@ -23,6 +23,7 @@ import {
23 23
     makeAspectRatioAware
24 24
 } from '../../base/responsive-ui';
25 25
 import { ColorPalette } from '../../base/styles';
26
+import { requestPipMode } from '../../mobile/picture-in-picture';
26 27
 import { beginRoomLockRequest } from '../../room-lock';
27 28
 import { beginShareRoom } from '../../share-room';
28 29
 
@@ -80,6 +81,11 @@ class Toolbox extends Component {
80 81
          */
81 82
         _onHangup: PropTypes.func,
82 83
 
84
+        /**
85
+         * Requests Picture-in-Picture mode.
86
+         */
87
+        _onPipRequest: PropTypes.func,
88
+
83 89
         /**
84 90
          * Sets the lock i.e. password protection of the conference/room.
85 91
          */
@@ -101,6 +107,11 @@ class Toolbox extends Component {
101 107
          */
102 108
         _onToggleCameraFacingMode: PropTypes.func,
103 109
 
110
+        /**
111
+         * Flag showing whether Picture-in-Picture is available.
112
+         */
113
+        _pipAvailable: PropTypes.bool,
114
+
104 115
         /**
105 116
          * Flag showing whether video is muted.
106 117
          */
@@ -296,6 +307,7 @@ class Toolbox extends Component {
296 307
         const underlayColor = 'transparent';
297 308
         const {
298 309
             _audioOnly: audioOnly,
310
+            _pipAvailable: pipAvailable,
299 311
             _videoMuted: videoMuted
300 312
         } = this.props;
301 313
 
@@ -305,6 +317,15 @@ class Toolbox extends Component {
305 317
             <View
306 318
                 key = 'secondaryToolbar'
307 319
                 style = { styles.secondaryToolbar }>
320
+                {
321
+                    pipAvailable
322
+                        && <ToolbarButton
323
+                            iconName = { 'menu-down' }
324
+                            iconStyle = { iconStyle }
325
+                            onClick = { this.props._onPipRequest }
326
+                            style = { style }
327
+                            underlayColor = { underlayColor } />
328
+                }
308 329
                 {
309 330
                     AudioRouteButton
310 331
                         && <AudioRouteButton
@@ -391,6 +412,17 @@ function _mapDispatchToProps(dispatch) {
391 412
     return {
392 413
         ...abstractMapDispatchToProps(dispatch),
393 414
 
415
+        /**
416
+         * Requests Picture-in-Picture mode.
417
+         *
418
+         * @private
419
+         * @returns {void}
420
+         * @type {Function}
421
+         */
422
+        _onPipRequest() {
423
+            dispatch(requestPipMode());
424
+        },
425
+
394 426
         /**
395 427
          * Sets the lock i.e. password protection of the conference/room.
396 428
          *
@@ -451,6 +483,7 @@ function _mapDispatchToProps(dispatch) {
451 483
 function _mapStateToProps(state) {
452 484
     const conference = state['features/base/conference'];
453 485
     const { enabled } = state['features/toolbox'];
486
+    const { app } = state['features/app'];
454 487
 
455 488
     return {
456 489
         ...abstractMapStateToProps(state),
@@ -479,7 +512,15 @@ function _mapStateToProps(state) {
479 512
          * @protected
480 513
          * @type {boolean}
481 514
          */
482
-        _locked: Boolean(conference.locked)
515
+        _locked: Boolean(conference.locked),
516
+
517
+        /**
518
+         * The indicator which determines if Picture-in-Picture is available.
519
+         *
520
+         * @protected
521
+         * @type {boolean}
522
+         */
523
+        _pipAvailable: Boolean(app && app.props.pipAvailable)
483 524
     };
484 525
 }
485 526
 

Loading…
Cancel
Save