diff --git a/Example/app/jsx/App.js b/Example/app/jsx/App.js index cacbe0a..b8d1a53 100644 --- a/Example/app/jsx/App.js +++ b/Example/app/jsx/App.js @@ -13,6 +13,7 @@ import DRMExample from './screens/DRMExample'; import LocalFileExample from './screens/LocalFileExample'; import SourcesExample from './screens/SourcesExample'; import YoutubeExample from './screens/YoutubeExample'; +import PlayerInModal from './screens/PlayerInModal'; const Stack = createNativeStackNavigator(); @@ -23,6 +24,7 @@ export default class App extends Component { + diff --git a/Example/app/jsx/components/Player.js b/Example/app/jsx/components/Player.js index 526179d..9849cfa 100644 --- a/Example/app/jsx/components/Player.js +++ b/Example/app/jsx/components/Player.js @@ -23,6 +23,7 @@ export default forwardRef((props, ref) => { config={{ license: Platform.select({ios: IOS_API_KEY, android: ANDROID_API_KEY}), backgroundAudioEnabled: true, + fullScreenOnLandscape: false, styling: { colors: {}, }, diff --git a/Example/app/jsx/screens/Home.js b/Example/app/jsx/screens/Home.js index 4aae7da..8bd56e0 100644 --- a/Example/app/jsx/screens/Home.js +++ b/Example/app/jsx/screens/Home.js @@ -3,7 +3,7 @@ import {StyleSheet, View, Text, TouchableOpacity, FlatList} from 'react-native'; import Icons from 'react-native-vector-icons/FontAwesome5'; import {useNavigation} from '@react-navigation/native'; -const SCREENS = ['Single', 'List', 'DRM', 'Local', 'Sources', 'Youtube']; +const SCREENS = ['Single', 'Modal', 'List', 'DRM', 'Local', 'Sources', 'Youtube']; export default () => { const navigation = useNavigation(); diff --git a/Example/app/jsx/screens/PlayerInModal.js b/Example/app/jsx/screens/PlayerInModal.js new file mode 100644 index 0000000..e61707a --- /dev/null +++ b/Example/app/jsx/screens/PlayerInModal.js @@ -0,0 +1,89 @@ +import React, { useRef, useState } from 'react'; +import Player from '../components/Player'; +import PlayerContainer from '../components/PlayerContainer'; +import { Alert, Modal, StyleSheet, Text, Pressable, View, StatusBar, Dimensions } from 'react-native'; +export const { height } = Dimensions.get('window'); + + +export default () => { + const playerRef = useRef([]); + + const [modalVisible, setModalVisible] = useState(false); + + let jwConfig = { + "title": "Basic Modal implementation", + "fullScreenOnLandscape": true, + "landscapeOnFullScreen": true, + "playerInModal": true, + "playlist": [ + { + "file": "https://content.bitsontherun.com/videos/q1fx20VZ-52qL9xLP.mp4", + } + ] + } + + return ( + + { + setModalVisible(!modalVisible); + }}> + + + + setModalVisible(false)}> + Hide Modal + + + setModalVisible(true)}> + Show Modal + + + + ); +}; +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + marginTop: 22, + }, + modalView: { + marginTop:height / 3, + height: 300 + }, + button: { + borderRadius: 20, + padding: 10, + elevation: 2, + }, + buttonOpen: { + backgroundColor: '#F194FF', + }, + buttonClose: { + backgroundColor: '#2196F3', + }, + textStyle: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center', + }, +}); \ No newline at end of file diff --git a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java index 064a009..eb70d6b 100755 --- a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +++ b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java @@ -12,6 +12,8 @@ import android.media.AudioFocusRequest; import android.media.AudioManager; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -96,7 +98,12 @@ import com.jwplayer.pub.api.events.listeners.CastingEvents; import com.jwplayer.pub.api.events.listeners.PipPluginEvents; import com.jwplayer.pub.api.events.listeners.VideoPlayerEvents; +import com.jwplayer.pub.api.fullscreen.ExtensibleFullscreenHandler; +import com.jwplayer.pub.api.fullscreen.FullscreenDialog; import com.jwplayer.pub.api.fullscreen.FullscreenHandler; +import com.jwplayer.pub.api.fullscreen.delegates.DeviceOrientationDelegate; +import com.jwplayer.pub.api.fullscreen.delegates.DialogLayoutDelegate; +import com.jwplayer.pub.api.fullscreen.delegates.SystemUiDelegate; import com.jwplayer.pub.api.license.LicenseUtil; import com.jwplayer.pub.api.media.playlists.PlaylistItem; import com.jwplayer.ui.views.CueMarkerSeekbar; @@ -183,6 +190,7 @@ public class RNJWPlayerView extends RelativeLayout implements Boolean fullScreenOnLandscape = false; Boolean portraitOnExitFullScreen = false; Boolean exitFullScreenOnPortrait = false; + Boolean playerInModal = false; Number currentPlayingIndex; @@ -427,12 +435,116 @@ public void setupPlayerView(Boolean backgroundAudioEnabled) { EventType.PIP_OPEN ); - mPlayer.setFullscreenHandler(new fullscreenHandler()); + if (playerInModal) { + mPlayer.setFullscreenHandler(createModalFullscreenHandler()); + } else { + mPlayer.setFullscreenHandler(new fullscreenHandler()); + } mPlayer.allowBackgroundAudio(backgroundAudioEnabled); } } + /** + * Helper to build the a generic `ExtensibleFullscreenHandler` with small tweaks to play nice with Modals + * @return {@link ExtensibleFullscreenHandler} + */ + private ExtensibleFullscreenHandler createModalFullscreenHandler() { + DeviceOrientationDelegate delegate = getDeviceOrientationDelegate(); + FullscreenDialog dialog = new FullscreenDialog( + mActivity, + mActivity, + android.R.style.Theme_Black_NoTitleBar_Fullscreen + ); + + return new ExtensibleFullscreenHandler( + new DialogLayoutDelegate( + mPlayerView, + dialog + ), + delegate, + new SystemUiDelegate( + mActivity, + mActivity.getLifecycle(), + new Handler(), + dialog.getWindow().getDecorView() + ) + ) { + @Override + public void onFullscreenRequested() { + // if landscape is priorty we have to turn off full-screen portrait before allowing + // the default call for full-screen + mPlayer.allowFullscreenPortrait(!landscapeOnFullScreen); + super.onFullscreenRequested(); + // safely set it back on UI thread after work can be finished + final Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed(() -> { + if (mPlayer != null) { + mPlayer.allowFullscreenPortrait(true); + } + }, 100); + WritableMap eventEnterFullscreen = Arguments.createMap(); + eventEnterFullscreen.putString("message", "onFullscreenRequested"); + getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent( + getId(), + "topFullScreenRequested", + eventEnterFullscreen); + } + + @Override + public void onFullscreenExitRequested() { + super.onFullscreenExitRequested(); + + WritableMap eventExitFullscreen = Arguments.createMap(); + eventExitFullscreen.putString("message", "onFullscreenExitRequested"); + getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent( + getId(), + "topFullScreenExitRequested", + eventExitFullscreen); + } + }; + } + + /** + * Add logic here for your custom orientation implementation + * + * @return Default {@link DeviceOrientationDelegate} + */ + private DeviceOrientationDelegate getDeviceOrientationDelegate() { + DeviceOrientationDelegate delegate = new DeviceOrientationDelegate( + mActivity, + mActivity.getLifecycle(), + new Handler() + ) { + @Override + public void setFullscreen(boolean fullscreen) { + super.setFullscreen(fullscreen); + } + + @Override + public void onAllowRotationChanged(boolean allowRotation) { + super.onAllowRotationChanged(allowRotation); + } + + @Override + protected void doRotation(boolean fullscreen, boolean allowFullscreenPortrait) { + super.doRotation(fullscreen, allowFullscreenPortrait); + } + + @Override + protected void doRotationListener() { + super.doRotationListener(); + } + + @Override + public void onAllowFullscreenPortrait(boolean allowFullscreenPortrait) { + super.onAllowFullscreenPortrait(allowFullscreenPortrait); + } + }; + delegate.onAllowRotationChanged(true); + return delegate; + } + private class fullscreenHandler implements FullscreenHandler { ViewGroup mPlayerViewContainer = (ViewGroup) mPlayerView.getParent(); private View mDecorView; @@ -504,8 +616,15 @@ public void run() { mPlayerViewContainer.addView(mPlayerView, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - mPlayerView.layout(mPlayerViewContainer.getLeft(), mPlayerViewContainer.getTop(), - mPlayerViewContainer.getRight(), mPlayerViewContainer.getBottom()); + // returning from full-screen portrait requires a different measure + if (mActivity.getResources().getConfiguration().orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + ) { + mPlayerView.layout(mPlayerView.getLeft(), mPlayerViewContainer.getTop(), + mPlayerViewContainer.getMeasuredWidth(), mPlayerViewContainer.getBottom()); + } else { + mPlayerView.layout(mPlayerViewContainer.getLeft(), mPlayerViewContainer.getTop(), + mPlayerViewContainer.getRight(), mPlayerViewContainer.getBottom()); + } } }); @@ -761,6 +880,18 @@ private void setupPlayer(ReadableMap prop) { mPlayerView.fullScreenOnLandscape = fullScreenOnLandscape; } + if (prop.hasKey("landscapeOnFullScreen")) { + landscapeOnFullScreen = prop.getBoolean("landscapeOnFullScreen"); + } + + if (prop.hasKey("portraitOnExitFullScreen")) { + portraitOnExitFullScreen = prop.getBoolean("fullScreenOnLandscape"); + } + + if (prop.hasKey("playerInModal")) { + playerInModal = prop.getBoolean("playerInModal"); + } + if (prop.hasKey("exitFullScreenOnPortrait")) { exitFullScreenOnPortrait = prop.getBoolean("exitFullScreenOnPortrait"); mPlayerView.exitFullScreenOnPortrait = exitFullScreenOnPortrait; diff --git a/index.d.ts b/index.d.ts index 30f25e6..ebba7f3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -448,6 +448,7 @@ declare module "@jwplayer/jwplayer-react-native" { landscapeOnFullScreen?: boolean; portraitOnExitFullScreen?: boolean; exitFullScreenOnPortrait?: boolean; + playerInModal?: boolean; playlist?: PlaylistItem[]; stretching?: string; related?: Related; diff --git a/index.js b/index.js index fff19e6..12852af 100644 --- a/index.js +++ b/index.js @@ -306,6 +306,7 @@ export default class JWPlayer extends Component { offlineImage: PropTypes.string, forceFullScreenOnLandscape: PropTypes.bool, forceLandscapeOnFullScreen: PropTypes.bool, + playerInModal: PropTypes.bool, enableLockScreenControls: PropTypes.bool, stretching: PropTypes.oneOf([ 'uniform',