Просмотр исходного кода

[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 лет назад
Родитель
Сommit
b3683068d4

+ 44
- 3
android/README.md Просмотреть файл

118
     }
118
     }
119
 
119
 
120
     @Override
120
     @Override
121
-    protected void onPause() {
122
-        super.onPause();
121
+    protected void onStop() {
122
+        super.onStop();
123
 
123
 
124
         JitsiMeetView.onHostPause(this);
124
         JitsiMeetView.onHostPause(this);
125
     }
125
     }
142
 
142
 
143
 See JitsiMeetView.getDefaultURL.
143
 See JitsiMeetView.getDefaultURL.
144
 
144
 
145
+#### getPictureInPictureAvailable()
146
+
147
+See JitsiMeetView.getPictureInPictureAvailable.
148
+
145
 #### getWelcomePageEnabled()
149
 #### getWelcomePageEnabled()
146
 
150
 
147
 See JitsiMeetView.getWelcomePageEnabled.
151
 See JitsiMeetView.getWelcomePageEnabled.
154
 
158
 
155
 See JitsiMeetView.setDefaultURL.
159
 See JitsiMeetView.setDefaultURL.
156
 
160
 
161
+#### setPictureInPictureAvailable(Boolean)
162
+
163
+See JitsiMeetView.setPictureInPictureAvailable.
164
+
157
 #### setWelcomePageEnabled(boolean)
165
 #### setWelcomePageEnabled(boolean)
158
 
166
 
159
 See JitsiMeetView.setWelcomePageEnabled.
167
 See JitsiMeetView.setWelcomePageEnabled.
179
 
187
 
180
 Returns the `JitsiMeetViewListener` instance attached to the view.
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
 #### getWelcomePageEnabled()
196
 #### getWelcomePageEnabled()
183
 
197
 
184
 Returns true if the Welcome page is enabled; otherwise, false. If false, a black
198
 Returns true if the Welcome page is enabled; otherwise, false. If false, a black
227
 Sets the given listener (class implementing the `JitsiMeetViewListener`
241
 Sets the given listener (class implementing the `JitsiMeetViewListener`
228
 interface) on the view.
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
 #### setWelcomePageEnabled(boolean)
251
 #### setWelcomePageEnabled(boolean)
231
 
252
 
232
 Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
253
 Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
257
 
278
 
258
 #### onHostResume(activity)
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
 This is a static method.
284
 This is a static method.
263
 
285
 
269
 
291
 
270
 This is a static method.
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
 #### JitsiMeetViewListener
301
 #### JitsiMeetViewListener
273
 
302
 
274
 `JitsiMeetViewListener` provides an interface apps can implement to listen to
303
 `JitsiMeetViewListener` provides an interface apps can implement to listen to
385
 -keep class org.jitsi.meet.sdk.** { *; }
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 Просмотреть файл

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

+ 3
- 1
android/app/src/main/java/org/jitsi/meet/MainActivity.java Просмотреть файл

95
     @Override
95
     @Override
96
     protected void onCreate(Bundle savedInstanceState) {
96
     protected void onCreate(Bundle savedInstanceState) {
97
         // As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do
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
         // SDK at the time of this writing but it is clearer to be explicit
101
         // SDK at the time of this writing but it is clearer to be explicit
100
         // about what we want anyway.
102
         // about what we want anyway.
101
         setWelcomePageEnabled(true);
103
         setWelcomePageEnabled(true);

+ 42
- 10
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java Просмотреть файл

17
 package org.jitsi.meet.sdk;
17
 package org.jitsi.meet.sdk;
18
 
18
 
19
 import android.content.Intent;
19
 import android.content.Intent;
20
+import android.content.res.Configuration;
20
 import android.net.Uri;
21
 import android.net.Uri;
21
 import android.os.Build;
22
 import android.os.Build;
22
 import android.os.Bundle;
23
 import android.os.Bundle;
41
  * hooked to the React Native subsystem via proxy calls through the
42
  * hooked to the React Native subsystem via proxy calls through the
42
  * {@code JKConferenceView} static methods.
43
  * {@code JKConferenceView} static methods.
43
  */
44
  */
44
-public class JitsiMeetActivity
45
-    extends AppCompatActivity {
46
-
45
+public class JitsiMeetActivity extends AppCompatActivity {
47
     /**
46
     /**
48
      * The request code identifying requests for the permission to draw on top
47
      * The request code identifying requests for the permission to draw on top
49
      * of other apps. The value must be 16-bit and is arbitrarily chosen here.
48
      * of other apps. The value must be 16-bit and is arbitrarily chosen here.
69
      */
68
      */
70
     private JitsiMeetView view;
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
      * Whether the Welcome page is enabled. The value is used only while
78
      * Whether the Welcome page is enabled. The value is used only while
74
      * {@link #view} equals {@code null}.
79
      * {@link #view} equals {@code null}.
91
         return view == null ? defaultURL : view.getDefaultURL();
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
      * @see JitsiMeetView#getWelcomePageEnabled()
110
      * @see JitsiMeetView#getWelcomePageEnabled()
123
         // XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
137
         // XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
124
         // is documented to need such an order in order to take effect:
138
         // is documented to need such an order in order to take effect:
125
         view.setDefaultURL(defaultURL);
139
         view.setDefaultURL(defaultURL);
140
+        view.setPictureInPictureAvailable(pipAvailable);
126
         view.setWelcomePageEnabled(welcomePageEnabled);
141
         view.setWelcomePageEnabled(welcomePageEnabled);
127
 
142
 
128
         view.loadURL(null);
143
         view.loadURL(null);
224
     }
239
     }
225
 
240
 
226
     @Override
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
         JitsiMeetView.onHostPause(this);
253
         JitsiMeetView.onHostPause(this);
231
         defaultBackButtonImpl = null;
254
         defaultBackButtonImpl = null;
232
     }
255
     }
233
 
256
 
234
     @Override
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
         }
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
      * @see JitsiMeetView#setWelcomePageEnabled(boolean)
288
      * @see JitsiMeetView#setWelcomePageEnabled(boolean)

+ 75
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java Просмотреть файл

21
 import android.content.Context;
21
 import android.content.Context;
22
 import android.content.Intent;
22
 import android.content.Intent;
23
 import android.net.Uri;
23
 import android.net.Uri;
24
+import android.os.Build;
24
 import android.os.Bundle;
25
 import android.os.Bundle;
25
 import android.support.annotation.NonNull;
26
 import android.support.annotation.NonNull;
26
 import android.support.annotation.Nullable;
27
 import android.support.annotation.Nullable;
30
 import com.facebook.react.ReactRootView;
31
 import com.facebook.react.ReactRootView;
31
 import com.facebook.react.bridge.NativeModule;
32
 import com.facebook.react.bridge.NativeModule;
32
 import com.facebook.react.bridge.ReactApplicationContext;
33
 import com.facebook.react.bridge.ReactApplicationContext;
34
+import com.facebook.react.bridge.ReactContext;
35
+import com.facebook.react.bridge.WritableMap;
33
 import com.facebook.react.common.LifecycleState;
36
 import com.facebook.react.common.LifecycleState;
34
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
37
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
38
+import com.facebook.react.modules.core.DeviceEventManagerModule;
35
 import com.rnimmersive.RNImmersiveModule;
39
 import com.rnimmersive.RNImmersiveModule;
36
 
40
 
37
 import java.net.URL;
41
 import java.net.URL;
65
             new AppInfoModule(reactContext),
69
             new AppInfoModule(reactContext),
66
             new AudioModeModule(reactContext),
70
             new AudioModeModule(reactContext),
67
             new ExternalAPIModule(reactContext),
71
             new ExternalAPIModule(reactContext),
72
+            new PictureInPictureModule(reactContext),
68
             new ProximityModule(reactContext),
73
             new ProximityModule(reactContext),
69
             new WiFiStatsModule(reactContext)
74
             new WiFiStatsModule(reactContext)
70
         );
75
         );
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
      * The default base {@code URL} used to join a conference when a partial URL
282
      * The default base {@code URL} used to join a conference when a partial URL
248
      * (e.g. a room name only) is specified to {@link #loadURLString(String)} or
283
      * (e.g. a room name only) is specified to {@link #loadURLString(String)} or
264
      */
299
      */
265
     private JitsiMeetViewListener listener;
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
      * React Native root view.
309
      * React Native root view.
269
      */
310
      */
328
         return listener;
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
      * Gets whether the Welcome page is enabled. If {@code true}, the Welcome
384
      * Gets whether the Welcome page is enabled. If {@code true}, the Welcome
333
      * page is rendered when this {@code JitsiMeetView} is not at a URL
385
      * page is rendered when this {@code JitsiMeetView} is not at a URL
369
         if (defaultURL != null) {
421
         if (defaultURL != null) {
370
             props.putString("defaultURL", defaultURL.toString());
422
             props.putString("defaultURL", defaultURL.toString());
371
         }
423
         }
424
+
372
         // externalAPIScope
425
         // externalAPIScope
373
         props.putString("externalAPIScope", externalAPIScope);
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
         // url
438
         // url
375
         if (urlObject != null) {
439
         if (urlObject != null) {
376
             props.putBundle("url", urlObject);
440
             props.putBundle("url", urlObject);
377
         }
441
         }
442
+
378
         // welcomePageEnabled
443
         // welcomePageEnabled
379
         props.putBoolean("welcomePageEnabled", welcomePageEnabled);
444
         props.putBoolean("welcomePageEnabled", welcomePageEnabled);
380
 
445
 
462
         this.listener = listener;
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
      * Sets whether the Welcome page is enabled. Must be called before
541
      * Sets whether the Welcome page is enabled. Must be called before
467
      * {@link #loadURL(URL)} for it to take effect.
542
      * {@link #loadURL(URL)} for it to take effect.

+ 61
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java Просмотреть файл

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 Просмотреть файл

55
 
55
 
56
 NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
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
 #### welcomePageEnabled
65
 #### welcomePageEnabled
59
 
66
 
60
 Property to get/set whether the Welcome page is enabled. If `NO`, a black empty
67
 Property to get/set whether the Welcome page is enabled. If `NO`, a black empty
178
 The `data` dictionary contains an "error" key with the error and a "url" key
185
 The `data` dictionary contains an "error" key with the error and a "url" key
179
 with the conference URL which necessitated the loading of the configuration
186
 with the conference URL which necessitated the loading of the configuration
180
 file.
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 Просмотреть файл

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

+ 21
- 1
ios/sdk/src/JitsiMeetView.m Просмотреть файл

109
 
109
 
110
 @end
110
 @end
111
 
111
 
112
-@implementation JitsiMeetView
112
+@implementation JitsiMeetView {
113
+    NSNumber *_pipAvailable;
114
+}
115
+
116
+@dynamic pipAvailable;
113
 
117
 
114
 static RCTBridgeWrapper *bridgeWrapper;
118
 static RCTBridgeWrapper *bridgeWrapper;
115
 
119
 
265
     }
269
     }
266
 
270
 
267
     props[@"externalAPIScope"] = externalAPIScope;
271
     props[@"externalAPIScope"] = externalAPIScope;
272
+    props[@"pipAvailable"] = @(self.pipAvailable);
268
     props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
273
     props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
269
 
274
 
270
     // XXX If urlObject is nil, then it must appear as undefined in the
275
     // XXX If urlObject is nil, then it must appear as undefined in the
315
     [self loadURLObject:urlString ? @{ @"url": urlString } : nil];
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
 #pragma mark Private methods
338
 #pragma mark Private methods
319
 
339
 
320
 /**
340
 /**

+ 9
- 0
ios/sdk/src/JitsiMeetViewDelegate.h Просмотреть файл

65
  */
65
  */
66
 - (void)loadConfigError:(NSDictionary *)data;
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
 @end
77
 @end

+ 7
- 0
react/features/app/components/App.native.js Просмотреть файл

17
 import '../../mobile/external-api';
17
 import '../../mobile/external-api';
18
 import '../../mobile/full-screen';
18
 import '../../mobile/full-screen';
19
 import '../../mobile/permissions';
19
 import '../../mobile/permissions';
20
+import '../../mobile/picture-in-picture';
20
 import '../../mobile/proximity';
21
 import '../../mobile/proximity';
21
 import '../../mobile/wake-lock';
22
 import '../../mobile/wake-lock';
22
 
23
 
36
     static propTypes = {
37
     static propTypes = {
37
         ...AbstractApp.propTypes,
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
          * Whether the Welcome page is enabled. If {@code true}, the Welcome
47
          * Whether the Welcome page is enabled. If {@code true}, the Welcome
41
          * page is rendered when the {@link App} is not at a location (URL)
48
          * page is rendered when the {@link App} is not at a location (URL)

+ 6
- 0
react/features/mobile/external-api/middleware.js Просмотреть файл

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

+ 22
- 0
react/features/mobile/picture-in-picture/actionTypes.js Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

23
     makeAspectRatioAware
23
     makeAspectRatioAware
24
 } from '../../base/responsive-ui';
24
 } from '../../base/responsive-ui';
25
 import { ColorPalette } from '../../base/styles';
25
 import { ColorPalette } from '../../base/styles';
26
+import { requestPipMode } from '../../mobile/picture-in-picture';
26
 import { beginRoomLockRequest } from '../../room-lock';
27
 import { beginRoomLockRequest } from '../../room-lock';
27
 import { beginShareRoom } from '../../share-room';
28
 import { beginShareRoom } from '../../share-room';
28
 
29
 
80
          */
81
          */
81
         _onHangup: PropTypes.func,
82
         _onHangup: PropTypes.func,
82
 
83
 
84
+        /**
85
+         * Requests Picture-in-Picture mode.
86
+         */
87
+        _onPipRequest: PropTypes.func,
88
+
83
         /**
89
         /**
84
          * Sets the lock i.e. password protection of the conference/room.
90
          * Sets the lock i.e. password protection of the conference/room.
85
          */
91
          */
101
          */
107
          */
102
         _onToggleCameraFacingMode: PropTypes.func,
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
          * Flag showing whether video is muted.
116
          * Flag showing whether video is muted.
106
          */
117
          */
296
         const underlayColor = 'transparent';
307
         const underlayColor = 'transparent';
297
         const {
308
         const {
298
             _audioOnly: audioOnly,
309
             _audioOnly: audioOnly,
310
+            _pipAvailable: pipAvailable,
299
             _videoMuted: videoMuted
311
             _videoMuted: videoMuted
300
         } = this.props;
312
         } = this.props;
301
 
313
 
305
             <View
317
             <View
306
                 key = 'secondaryToolbar'
318
                 key = 'secondaryToolbar'
307
                 style = { styles.secondaryToolbar }>
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
                     AudioRouteButton
330
                     AudioRouteButton
310
                         && <AudioRouteButton
331
                         && <AudioRouteButton
391
     return {
412
     return {
392
         ...abstractMapDispatchToProps(dispatch),
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
          * Sets the lock i.e. password protection of the conference/room.
427
          * Sets the lock i.e. password protection of the conference/room.
396
          *
428
          *
451
 function _mapStateToProps(state) {
483
 function _mapStateToProps(state) {
452
     const conference = state['features/base/conference'];
484
     const conference = state['features/base/conference'];
453
     const { enabled } = state['features/toolbox'];
485
     const { enabled } = state['features/toolbox'];
486
+    const { app } = state['features/app'];
454
 
487
 
455
     return {
488
     return {
456
         ...abstractMapStateToProps(state),
489
         ...abstractMapStateToProps(state),
479
          * @protected
512
          * @protected
480
          * @type {boolean}
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
 

Загрузка…
Отмена
Сохранить