[RN] Add Picture-in-Picture support (Coding style: naming, consistency)

This commit is contained in:
Lyubo Marinov 2018-02-19 16:52:21 -06:00
parent b3683068d4
commit b8de5bbfc3
23 changed files with 481 additions and 378 deletions

View File

@ -117,19 +117,19 @@ public class MainActivity extends AppCompatActivity {
JitsiMeetView.onNewIntent(intent);
}
@Override
protected void onStop() {
super.onStop();
JitsiMeetView.onHostPause(this);
}
@Override
protected void onResume() {
super.onResume();
JitsiMeetView.onHostResume(this);
}
@Override
protected void onStop() {
super.onStop();
JitsiMeetView.onHostPause(this);
}
}
```
@ -142,9 +142,9 @@ which displays a single `JitsiMeetView`.
See JitsiMeetView.getDefaultURL.
#### getPictureInPictureAvailable()
#### getPictureInPictureEnabled()
See JitsiMeetView.getPictureInPictureAvailable.
See JitsiMeetView.getPictureInPictureEnabled.
#### getWelcomePageEnabled()
@ -158,9 +158,9 @@ See JitsiMeetView.loadURL.
See JitsiMeetView.setDefaultURL.
#### setPictureInPictureAvailable(Boolean)
#### setPictureInPictureEnabled(boolean)
See JitsiMeetView.setPictureInPictureAvailable.
See JitsiMeetView.setPictureInPictureEnabled.
#### setWelcomePageEnabled(boolean)
@ -187,11 +187,11 @@ if set to `null`, the default built in JavaScript is used: https://meet.jit.si.
Returns the `JitsiMeetViewListener` instance attached to the view.
#### getPictureInPictureAvailable()
#### getPictureInPictureEnabled()
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.
Returns `true` if Picture-in-Picture is enabled; `false`, otherwise. If not
explicitly set (by a preceding `setPictureInPictureEnabled` call), defaults to
`true` if the platform supports Picture-in-Picture natively; `false`, otherwise.
#### getWelcomePageEnabled()
@ -234,26 +234,30 @@ view.loadURLObject(urlObject);
Sets the default URL. See `getDefaultURL` for more information.
NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
NOTE: Must be called before (if at all) `loadURL`/`loadURLString` for it to take
effect.
#### setListener(listener)
Sets the given listener (class implementing the `JitsiMeetViewListener`
interface) on the view.
#### setPictureInPictureAvailable(Boolean)
#### setPictureInPictureEnabled(boolean)
Sets wether Picture-in-Picture is available. When set to `null` if will be
detected at runtime based on platform support.
Sets whether Picture-in-Picture is enabled. If not set, Jitsi Meet SDK
automatically enables/disables Picture-in-Picture based on native platform
support.
NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
NOTE: Must be called (if at all) before `loadURL`/`loadURLString` for it to take
effect.
#### setWelcomePageEnabled(boolean)
Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
information.
NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
NOTE: Must be called (if at all) before `loadURL`/`loadURLString` for it to take
effect.
#### onBackPressed()
@ -416,13 +420,10 @@ rules file:
## 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.
`JitsiMeetView` will automatically adjust its UI when presented in a
Picture-in-Picture style scenario, in a rectangle too small to accommodate its
"full" UI.
Jitsi Meet SDK automatically enables (unless explicitly disabled by a
`setPictureInPictureEnabled(false)` call) Android's native Picture-in-Picture
mode iff the platform is supported i.e. Android >= Oreo.

View File

@ -7,7 +7,7 @@
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:launchMode="singleTask"
android:name=".MainActivity"

View File

@ -97,9 +97,8 @@ public class MainActivity extends JitsiMeetActivity {
// As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do
// 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
// about what we want anyway.
// The welcome page defaults to disabled in the SDK at the time of this
// writing but it is clearer to be explicit about what we want anyway.
setWelcomePageEnabled(true);
super.onCreate(savedInstanceState);

View File

@ -69,10 +69,10 @@ public class JitsiMeetActivity extends AppCompatActivity {
private JitsiMeetView view;
/**
* Whether Picture-in-Picture is available. The value is used only while
* Whether Picture-in-Picture is enabled. The value is used only while
* {@link #view} equals {@code null}.
*/
private Boolean pipAvailable;
private Boolean pictureInPictureEnabled;
/**
* Whether the Welcome page is enabled. The value is used only while
@ -98,11 +98,13 @@ public class JitsiMeetActivity extends AppCompatActivity {
/**
*
* @see JitsiMeetView#getPictureInPictureAvailable()
* @see JitsiMeetView#getPictureInPictureEnabled()
*/
public Boolean getPictureInPictureAvailable() {
return view == null
? pipAvailable : view.getPictureInPictureAvailable();
public boolean getPictureInPictureEnabled() {
return
view == null
? pictureInPictureEnabled
: view.getPictureInPictureEnabled();
}
/**
@ -137,7 +139,10 @@ public class JitsiMeetActivity extends AppCompatActivity {
// XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
// is documented to need such an order in order to take effect:
view.setDefaultURL(defaultURL);
view.setPictureInPictureAvailable(pipAvailable);
if (pictureInPictureEnabled != null) {
view.setPictureInPictureEnabled(
pictureInPictureEnabled.booleanValue());
}
view.setWelcomePageEnabled(welcomePageEnabled);
view.loadURL(null);
@ -273,13 +278,14 @@ public class JitsiMeetActivity extends AppCompatActivity {
/**
*
* @see JitsiMeetView#setPictureInPictureAvailable(Boolean)
* @see JitsiMeetView#setPictureInPictureEnabled(boolean)
*/
public void setPictureInPictureAvailable(Boolean pipAvailable) {
public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
if (view == null) {
this.pipAvailable = pipAvailable;
this.pictureInPictureEnabled
= Boolean.valueOf(pictureInPictureEnabled);
} else {
view.setPictureInPictureAvailable(pipAvailable);
view.setPictureInPictureEnabled(pictureInPictureEnabled);
}
}

View File

@ -300,10 +300,11 @@ public class JitsiMeetView extends FrameLayout {
private JitsiMeetViewListener listener;
/**
* Whether Picture-in-Picture is available. If {@code null} it will default
* to {@code true} iff the platform supports it.
* Whether Picture-in-Picture is enabled. If {@code null}, defaults to
* {@code true} iff the Android platform supports Picture-in-Picture
* natively.
*/
private Boolean pipAvailable;
private Boolean pictureInPictureEnabled;
/**
* React Native root view.
@ -370,14 +371,18 @@ public class JitsiMeetView extends FrameLayout {
}
/**
* 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.
* Gets whether Picture-in-Picture is enabled. Picture-in-Picture is
* natively 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.
* @return If Picture-in-Picture is enabled, {@code true}; {@code false},
* otherwise.
*/
public Boolean getPictureInPictureAvailable() {
return pipAvailable;
public boolean getPictureInPictureEnabled() {
return
PictureInPictureModule.isPictureInPictureSupported()
&& (pictureInPictureEnabled == null
|| pictureInPictureEnabled.booleanValue());
}
/**
@ -425,15 +430,10 @@ public class JitsiMeetView extends FrameLayout {
// 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_);
// pictureInPictureEnabled
props.putBoolean(
"pictureInPictureEnabled",
getPictureInPictureEnabled());
// url
if (urlObject != null) {
@ -457,7 +457,9 @@ public class JitsiMeetView extends FrameLayout {
if (reactRootView == null) {
reactRootView = new ReactRootView(getContext());
reactRootView.startReactApplication(
reactInstanceManager, "App", props);
reactInstanceManager,
"App",
props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
} else {
@ -528,13 +530,15 @@ public class JitsiMeetView extends FrameLayout {
}
/**
* Sets whether Picture-in-Picture is currently available.
* Sets whether Picture-in-Picture is enabled. Because Picture-in-Picture is
* natively supported only since certain platform versions, specifying
* {@code true} will have no effect on unsupported platform versions.
*
* @param pipAvailable {@code true} if PiP is available, {@code false}
* otherwise.
* @param pictureInPictureEnabled To enable Picture-in-Picture,
* {@code true}; otherwise, {@code false}.
*/
public void setPictureInPictureAvailable(Boolean pipAvailable) {
this.pipAvailable = pipAvailable;
public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
this.pictureInPictureEnabled = Boolean.valueOf(pictureInPictureEnabled);
}
/**

View File

@ -14,48 +14,54 @@ import com.facebook.react.bridge.ReactMethod;
public class PictureInPictureModule extends ReactContextBaseJavaModule {
private final static String TAG = "PictureInPicture";
static boolean isPictureInPictureSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
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.
* Enters Picture-in-Picture (mode) for the current {@link Activity}.
* Supported on Android API >= 26 (Oreo) only.
*
* @param promise a {@code Promise} which will resolve with a {@code null}
* value in case of success, and an error otherwise.
* value upon success, and an {@link Exception} otherwise.
*/
@ReactMethod
public void enterPictureInPictureMode(Promise promise) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
final Activity currentActivity = getCurrentActivity();
public void enterPictureInPicture(Promise promise) {
if (isPictureInPictureSupported()) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
promise.reject(new Exception("No current Activity!"));
return;
}
Log.d(TAG, "Entering PiP mode");
Log.d(TAG, "Entering Picture-in-Picture");
PictureInPictureParams.Builder builder
= new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(1, 1));
boolean r
= currentActivity.enterPictureInPictureMode(builder.build());
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"));
promise.reject(
new Exception("Failed to enter Picture-in-Picture"));
}
return;
}
promise.reject(new Exception("PiP not supported"));
promise.reject(new Exception("Picture-in-Picture not supported"));
}
@Override
public String getName() {
return TAG;
}
}

View File

@ -53,21 +53,24 @@ partial URL (e.g. a room name only) is specified to
`loadURLString:`/`loadURLObject:`. If not set or if set to `nil`, the default
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 (if at all) before `loadURL:`/`loadURLString:` for it to take
effect.
#### pipAvailable
#### pictureInPictureEnabled
Property to get / set wether a Picture-in-Picture mode is available. This must
be implemented by the application at the moment.
Property to get / set whether Picture-in-Picture is enabled. Defaults to `YES`
if `delegate` implements `enterPictureInPicture:`; otherwise, `NO`.
NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
NOTE: Must be set (if at all) before `loadURL:`/`loadURLString:` for it to take
effect.
#### welcomePageEnabled
Property to get/set whether the Welcome page is enabled. If `NO`, a black empty
view will be rendered when not in a conference. Defaults to `NO`.
NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
NOTE: Must be set (if at all) before `loadURL:`/`loadURLString:` for it to take
effect.
#### loadURL:NSURL
@ -177,6 +180,16 @@ Called before a conference is left.
The `data` dictionary contains a "url" key with the conference URL.
#### enterPictureInPicture
Called when entering Picture-in-Picture is requested by the user. The app should
now activate its Picture-in-Picture implementation (and resize the associated
`JitsiMeetView`. The latter will automatically detect its new size and adjust
its user interface to a variant appropriate for the small size ordinarily
associated with Picture-in-Picture.)
The `data` dictionary is empty.
#### loadConfigError
Called when loading the main configuration file from the Jitsi Meet deployment
@ -186,23 +199,16 @@ 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
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.
`JitsiMeetView` will automatically adjust its UI when presented in a
Picture-in-Picture style scenario, in a rectangle too small to accommodate its
"full" UI.
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.
Jitsi Meet SDK does not currently implement native Picture-in-Picture on iOS. If
desired, apps need to implement non-native Picture-in-Picture themselves and
resize `JitsiMeetView`.
If `pictureInPictureEnabled` is set to `YES` or `delegate` implements
`enterPictureInPicture:`, the in-call toolbar will render a button to afford the
user to request entering Picture-in-Picture.

View File

@ -25,7 +25,7 @@
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic) BOOL pipAvailable;
@property (nonatomic) BOOL pictureInPictureEnabled;
@property (nonatomic) BOOL welcomePageEnabled;

View File

@ -110,10 +110,10 @@ void registerFatalErrorHandler() {
@end
@implementation JitsiMeetView {
NSNumber *_pipAvailable;
NSNumber *_pictureInPictureEnabled;
}
@dynamic pipAvailable;
@dynamic pictureInPictureEnabled;
static RCTBridgeWrapper *bridgeWrapper;
@ -269,7 +269,7 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
}
props[@"externalAPIScope"] = externalAPIScope;
props[@"pipAvailable"] = @(self.pipAvailable);
props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
// XXX If urlObject is nil, then it must appear as undefined in the
@ -320,19 +320,26 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
[self loadURLObject:urlString ? @{ @"url": urlString } : nil];
}
#pragma pipAvailable getter / setter
#pragma pictureInPictureEnabled getter / setter
- (void) setPipAvailable:(BOOL)pipAvailable {
_pipAvailable = [NSNumber numberWithBool:pipAvailable];
- (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled {
_pictureInPictureEnabled
= [NSNumber numberWithBool:pictureInPictureEnabled];
}
- (BOOL) pipAvailable {
if (_pipAvailable == nil) {
return self.delegate
&& [self.delegate respondsToSelector:@selector(requestPipMode:)];
- (BOOL) pictureInPictureEnabled {
if (_pictureInPictureEnabled) {
return [_pictureInPictureEnabled boolValue];
}
return [_pipAvailable boolValue];
// The SDK/JitsiMeetView client/consumer did not explicitly enable/disable
// Picture-in-Picture. However, we may automatically deduce their
// intentions: we need the support of the client in order to implement
// Picture-in-Picture on iOS (in contrast to Android) so if the client
// appears to have provided the support then we can assume that they did it
// with the intention to have Picture-in-Picture enabled.
return self.delegate
&& [self.delegate respondsToSelector:@selector(enterPictureInPicture:)];
}
#pragma mark Private methods

View File

@ -55,6 +55,17 @@
*/
- (void)conferenceWillLeave:(NSDictionary *)data;
/**
* Called when entering Picture-in-Picture is requested by the user. The app
* should now activate its Picture-in-Picture implementation (and resize the
* associated `JitsiMeetView`. The latter will automatically detect its new size
* and adjust its user interface to a variant appropriate for the small size
* ordinarily associated with Picture-in-Picture.)
*
* The `data` dictionary is empty.
*/
- (void)enterPictureInPicture:(NSDictionary *)data;
/**
* Called when loading the main configuration file from the Jitsi Meet
* deployment file.
@ -65,13 +76,4 @@
*/
- (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

View File

@ -38,10 +38,11 @@ export class App extends AbstractApp {
...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.
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
* button is rendered in the {@link Conference} view to afford entering
* Picture-in-Picture.
*/
pipAvailable: PropTypes.bool,
pictureInPictureEnabled: PropTypes.bool,
/**
* Whether the Welcome page is enabled. If {@code true}, the Welcome

View File

@ -13,8 +13,7 @@ import {
import { LOAD_CONFIG_ERROR } from '../../base/config';
import { MiddlewareRegistry } from '../../base/redux';
import { toURLString } from '../../base/util';
import { REQUEST_PIP_MODE } from '../picture-in-picture';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
/**
* Middleware that captures Redux actions and uses the ExternalAPI module to
@ -55,6 +54,10 @@ MiddlewareRegistry.register(store => next => action => {
_sendConferenceEvent(store, action);
break;
case ENTER_PICTURE_IN_PICTURE:
_sendEvent(store, _getSymbolDescription(action.type), /* data */ {});
break;
case LOAD_CONFIG_ERROR: {
const { error, locationURL, type } = action;
@ -64,10 +67,6 @@ MiddlewareRegistry.register(store => next => action => {
});
break;
}
case REQUEST_PIP_MODE:
_sendEvent(store, _getSymbolDescription(action.type), /* data */ {});
}
return result;

View File

@ -1,22 +1,24 @@
/**
* The type of redux action to set the PiP related event listeners.
* The type of redux action to enter (or rather initiate entering)
* picture-in-picture.
*
* {
* 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
* type: ENTER_PICTURE_IN_PICTURE
* }
*
* @public
*/
export const REQUEST_PIP_MODE = Symbol('REQUEST_PIP_MODE');
export const ENTER_PICTURE_IN_PICTURE = Symbol('ENTER_PICTURE_IN_PICTURE');
/**
* The type of redux action to set the {@code EventEmitter} subscriptions
* utilized by the feature picture-in-picture.
*
* {
* type: _SET_EMITTER_SUBSCRIPTIONS,
* emitterSubscriptions: Array|undefined
* }
*
* @protected
*/
export const _SET_EMITTER_SUBSCRIPTIONS = Symbol('_SET_EMITTER_SUBSCRIPTIONS');

View File

@ -1,37 +1,60 @@
// @flow
import { NativeModules } from 'react-native';
import {
_SET_PIP_LISTENERS,
REQUEST_PIP_MODE
ENTER_PICTURE_IN_PICTURE,
_SET_EMITTER_SUBSCRIPTIONS
} from './actionTypes';
/**
* Sets the listeners for the PiP related events.
* Enters (or rather initiates entering) picture-in-picture.
* 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 {Array} listeners - Array of listeners to be set.
* @protected
* @returns {{
* type: _SET_PIP_LISTENERS,
* listeners: Array
* }}
* @public
* @returns {Function}
*/
export function _setListeners(listeners: ?Array<any>) {
return {
type: _SET_PIP_LISTENERS,
listeners
export function enterPictureInPicture() {
return (dispatch: Dispatch, getState: Function) => {
const state = getState();
const { app } = state['features/app'];
const { conference, joining } = state['features/base/conference'];
if (app
&& app.props.pictureInPictureEnabled
&& (conference || joining)) {
const { PictureInPicture } = NativeModules;
const p
= PictureInPicture
? PictureInPicture.enterPictureInPicture()
: Promise.reject(
new Error('Picture-in-Picture not supported'));
p.then(
() => dispatch({ type: ENTER_PICTURE_IN_PICTURE }),
e => console.warn(`Error entering PiP mode: ${e}`));
}
};
}
/**
* Requests Picture-in-Picture mode.
* Sets the {@code EventEmitter} subscriptions utilized by the feature
* picture-in-picture.
*
* @public
* @param {Array<Object>} emitterSubscriptions - The {@code EventEmitter}
* subscriptions to be set.
* @protected
* @returns {{
* type: REQUEST_PIP_MODE
* type: _SET_EMITTER_SUBSCRIPTIONS,
* emitterSubscriptions: Array<Object>
* }}
*/
export function requestPipMode() {
export function _setEmitterSubscriptions(emitterSubscriptions: ?Array<Object>) {
return {
type: REQUEST_PIP_MODE
type: _SET_EMITTER_SUBSCRIPTIONS,
emitterSubscriptions
};
}

View File

@ -0,0 +1,112 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ToolbarButton } from '../../../toolbox';
import { enterPictureInPicture } from '../actions';
/**
* The type of {@link EnterPictureInPictureToobarButton}'s React
* {@code Component} props.
*/
type Props = {
/**
* Enters (or rather initiates entering) picture-in-picture.
*
* @protected
*/
_onEnterPictureInPicture: Function,
/**
* The indicator which determines whether Picture-in-Picture is enabled.
*
* @protected
*/
_pictureInPictureEnabled: boolean
};
/**
* Implements a {@link ToolbarButton} to enter Picture-in-Picture.
*/
class EnterPictureInPictureToolbarButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
_onEnterPictureInPicture,
_pictureInPictureEnabled,
...props
} = this.props;
if (!_pictureInPictureEnabled) {
return null;
}
return (
<ToolbarButton
iconName = { 'menu-down' }
onClick = { _onEnterPictureInPicture }
{ ...props } />
);
}
}
/**
* Maps redux actions to {@link EnterPictureInPictureToolbarButton}'s React
* {@code Component} props.
*
* @param {Function} dispatch - The redux action {@code dispatch} function.
* @returns {{
* }}
* @private
*/
function _mapDispatchToProps(dispatch) {
return {
/**
* Requests Picture-in-Picture mode.
*
* @private
* @returns {void}
* @type {Function}
*/
_onEnterPictureInPicture() {
dispatch(enterPictureInPicture());
}
};
}
/**
* Maps (parts of) the redux state to
* {@link EnterPictureInPictureToolbarButton}'s React {@code Component} props.
*
* @param {Object} state - The redux store/state.
* @private
* @returns {{
* }}
*/
function _mapStateToProps(state) {
const { app } = state['features/app'];
return {
/**
* The indicator which determines whether Picture-in-Picture is enabled.
*
* @protected
* @type {boolean}
*/
_pictureInPictureEnabled:
Boolean(app && app.props.pictureInPictureEnabled)
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(
EnterPictureInPictureToolbarButton);

View File

@ -0,0 +1,2 @@
export { default as EnterPictureInPictureToolbarButton }
from './EnterPictureInPictureToolbarButton';

View File

@ -1,19 +0,0 @@
// @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'));
}

View File

@ -1,6 +1,6 @@
export * from './actions';
export * from './actionTypes';
export * from './functions';
export * from './components';
import './middleware';
import './reducer';

View File

@ -5,9 +5,8 @@ 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';
import { enterPictureInPicture, _setEmitterSubscriptions } from './actions';
import { _SET_EMITTER_SUBSCRIPTIONS } from './actionTypes';
/**
* Middleware that handles Picture-in-Picture requests. Currently it enters
@ -18,30 +17,28 @@ import { enterPictureInPictureMode } from './functions';
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case _SET_PIP_LISTENERS: {
// Remove the current/old listeners.
const { listeners } = store.getState()['features/pip'];
case APP_WILL_MOUNT:
return _appWillMount(store, next, action);
if (listeners) {
for (const listener of listeners) {
listener.remove();
case APP_WILL_UNMOUNT:
store.dispatch(_setEmitterSubscriptions(undefined));
break;
case _SET_EMITTER_SUBSCRIPTIONS: {
// Remove the current/old EventEmitter subscriptions.
const { emitterSubscriptions } = store.getState()['features/pip'];
if (emitterSubscriptions) {
for (const emitterSubscription of emitterSubscriptions) {
// XXX We may be removing an EventEmitter subscription which is
// in both the old and new Array of EventEmitter subscriptions!
// Thankfully, we don't have such a practical use case at the
// time of this writing.
emitterSubscription.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);
@ -58,43 +55,16 @@ MiddlewareRegistry.register(store => next => action => {
* @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
* being dispatched in the specified {@code store}.
* @private
* @returns {*}
* @returns {*} The value returned by {@code next(action)}.
*/
function _appWillMount({ dispatch, getState }) {
const context = {
dispatch,
getState
};
const listeners = [
function _appWillMount({ dispatch }, next, action) {
dispatch(_setEmitterSubscriptions([
// Android's onUserLeaveHint activity lifecycle callback
DeviceEventEmitter.addListener('onUserLeaveHint', () => {
_enterPictureInPicture(context);
})
];
DeviceEventEmitter.addListener(
'onUserLeaveHint',
() => dispatch(enterPictureInPicture()))
]));
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}`);
});
}
return next(action);
}

View File

@ -1,13 +1,15 @@
// @flow
import { ReducerRegistry } from '../../base/redux';
import { _SET_PIP_LISTENERS } from './actionTypes';
import { _SET_EMITTER_SUBSCRIPTIONS } from './actionTypes';
ReducerRegistry.register('features/pip', (state = {}, action) => {
switch (action.type) {
case _SET_PIP_LISTENERS:
case _SET_EMITTER_SUBSCRIPTIONS:
return {
...state,
listeners: action.listeners
emitterSubscriptions: action.emitterSubscriptions
};
}

View File

@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
// @flow
import React, { Component } from 'react';
import { View } from 'react-native';
import { connect } from 'react-redux';
@ -23,7 +24,9 @@ import {
makeAspectRatioAware
} from '../../base/responsive-ui';
import { ColorPalette } from '../../base/styles';
import { requestPipMode } from '../../mobile/picture-in-picture';
import {
EnterPictureInPictureToolbarButton
} from '../../mobile/picture-in-picture';
import { beginRoomLockRequest } from '../../room-lock';
import { beginShareRoom } from '../../share-room';
@ -46,92 +49,82 @@ import ToolbarButton from './ToolbarButton';
*/
const _SHARE_ROOM_TOOLBAR_BUTTON = true;
/**
* The type of {@link Toolbox}'s React {@code Component} props.
*/
type Props = {
/**
* Flag showing that audio is muted.
*/
_audioMuted: boolean,
/**
* Flag showing whether the audio-only mode is in use.
*/
_audioOnly: boolean,
/**
* The indicator which determines whether the toolbox is enabled.
*/
_enabled: boolean,
/**
* Flag showing whether room is locked.
*/
_locked: boolean,
/**
* Handler for hangup.
*/
_onHangup: Function,
/**
* Sets the lock i.e. password protection of the conference/room.
*/
_onRoomLock: Function,
/**
* Begins the UI procedure to share the conference/room URL.
*/
_onShareRoom: Function,
/**
* Toggles the audio-only flag of the conference.
*/
_onToggleAudioOnly: Function,
/**
* Switches between the front/user-facing and back/environment-facing
* cameras.
*/
_onToggleCameraFacingMode: Function,
/**
* Flag showing whether video is muted.
*/
_videoMuted: boolean,
/**
* Flag showing whether toolbar is visible.
*/
_visible: boolean,
dispatch: Function
};
/**
* Implements the conference toolbox on React Native.
*/
class Toolbox extends Component {
/**
* Toolbox component's property types.
*
* @static
*/
static propTypes = {
/**
* Flag showing that audio is muted.
*/
_audioMuted: PropTypes.bool,
/**
* Flag showing whether the audio-only mode is in use.
*/
_audioOnly: PropTypes.bool,
/**
* The indicator which determines whether the toolbox is enabled.
*/
_enabled: PropTypes.bool,
/**
* Flag showing whether room is locked.
*/
_locked: PropTypes.bool,
/**
* Handler for hangup.
*/
_onHangup: PropTypes.func,
/**
* Requests Picture-in-Picture mode.
*/
_onPipRequest: PropTypes.func,
/**
* Sets the lock i.e. password protection of the conference/room.
*/
_onRoomLock: PropTypes.func,
/**
* Begins the UI procedure to share the conference/room URL.
*/
_onShareRoom: PropTypes.func,
/**
* Toggles the audio-only flag of the conference.
*/
_onToggleAudioOnly: PropTypes.func,
/**
* Switches between the front/user-facing and back/environment-facing
* cameras.
*/
_onToggleCameraFacingMode: PropTypes.func,
/**
* Flag showing whether Picture-in-Picture is available.
*/
_pipAvailable: PropTypes.bool,
/**
* Flag showing whether video is muted.
*/
_videoMuted: PropTypes.bool,
/**
* Flag showing whether toolbar is visible.
*/
_visible: PropTypes.bool,
dispatch: PropTypes.func
};
class Toolbox extends Component<Props> {
/**
* Initializes a new {@code Toolbox} instance.
*
* @param {Object} props - The read-only React {@code Component} props with
* @param {Props} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props) {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
@ -183,22 +176,26 @@ class Toolbox extends Component {
let style;
if (this.props[`_${mediaType}Muted`]) {
iconName = this[`${mediaType}MutedIcon`];
iconName = `${mediaType}MutedIcon`;
iconStyle = styles.whitePrimaryToolbarButtonIcon;
style = styles.whitePrimaryToolbarButton;
} else {
iconName = this[`${mediaType}Icon`];
iconName = `${mediaType}Icon`;
iconStyle = styles.primaryToolbarButtonIcon;
style = styles.primaryToolbarButton;
}
return {
iconName,
// $FlowExpectedError
iconName: this[iconName],
iconStyle,
style
};
}
_onToggleAudio: () => void;
/**
* Dispatches an action to toggle the mute state of the audio/microphone.
*
@ -226,6 +223,8 @@ class Toolbox extends Component {
/* ensureTrack */ true));
}
_onToggleVideo: () => void;
/**
* Dispatches an action to toggle the mute state of the video/camera.
*
@ -307,7 +306,6 @@ class Toolbox extends Component {
const underlayColor = 'transparent';
const {
_audioOnly: audioOnly,
_pipAvailable: pipAvailable,
_videoMuted: videoMuted
} = this.props;
@ -317,15 +315,6 @@ class Toolbox extends Component {
<View
key = 'secondaryToolbar'
style = { styles.secondaryToolbar }>
{
pipAvailable
&& <ToolbarButton
iconName = { 'menu-down' }
iconStyle = { iconStyle }
onClick = { this.props._onPipRequest }
style = { style }
underlayColor = { underlayColor } />
}
{
AudioRouteButton
&& <AudioRouteButton
@ -364,6 +353,10 @@ class Toolbox extends Component {
style = { style }
underlayColor = { underlayColor } />
}
<EnterPictureInPictureToolbarButton
iconStyle = { iconStyle }
style = { style }
underlayColor = { underlayColor } />
</View>
);
@ -390,6 +383,7 @@ class Toolbox extends Component {
* TODO As soon as we have common font sets for web and native, this will no
* longer be required.
*/
// $FlowExpectedError
Object.assign(Toolbox.prototype, {
audioIcon: 'microphone',
audioMutedIcon: 'mic-disabled',
@ -398,31 +392,20 @@ Object.assign(Toolbox.prototype, {
});
/**
* Maps actions to React component props.
* Maps redux actions to {@link Toolbox}'s React {@code Component} props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @param {Function} dispatch - The redux action {@code dispatch} function.
* @private
* @returns {{
* _onRoomLock: Function,
* _onToggleAudioOnly: Function,
* _onToggleCameraFacingMode: Function,
* }}
* @private
*/
function _mapDispatchToProps(dispatch) {
return {
...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.
*
@ -471,19 +454,20 @@ function _mapDispatchToProps(dispatch) {
}
/**
* Maps part of Redux store to React component props.
* Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component}
* props.
*
* @param {Object} state - Redux store.
* @param {Object} state - The redux store/state.
* @private
* @returns {{
* _audioOnly: boolean,
* _enabled: boolean,
* _locked: boolean
* }}
* @private
*/
function _mapStateToProps(state) {
const conference = state['features/base/conference'];
const { enabled } = state['features/toolbox'];
const { app } = state['features/app'];
return {
...abstractMapStateToProps(state),
@ -512,15 +496,7 @@ function _mapStateToProps(state) {
* @protected
* @type {boolean}
*/
_locked: Boolean(conference.locked),
/**
* The indicator which determines if Picture-in-Picture is available.
*
* @protected
* @type {boolean}
*/
_pipAvailable: Boolean(app && app.props.pipAvailable)
_locked: Boolean(conference.locked)
};
}

View File

@ -1,21 +1,21 @@
/* @flow */
import type { Dispatch } from 'redux';
// @flow
import { appNavigate } from '../app';
import { MEDIA_TYPE } from '../base/media';
import { isLocalTrackMuted } from '../base/tracks';
import type { Dispatch } from 'redux';
/**
* Maps redux actions to {@link Toolbox} (React {@code Component}) props.
*
* @param {Function} dispatch - The redux {@code dispatch} function.
* @private
* @returns {{
* _onHangup: Function,
* _onToggleAudio: Function,
* _onToggleVideo: Function
* }}
* @private
*/
export function abstractMapDispatchToProps(dispatch: Dispatch<*>): Object {
return {

View File

@ -7,7 +7,11 @@ import getDefaultButtons from './defaultToolbarButtons';
declare var interfaceConfig: Object;
export { abstractMapStateToProps, getButton } from './functions.native';
export {
abstractMapDispatchToProps,
abstractMapStateToProps,
getButton
} from './functions.native';
/**
* Returns an object which contains the default buttons for the primary and