Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/android pip #1776

Closed
wants to merge 12 commits into from
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Changelog

### next
* Added PictureInPicture support for AndroidExoplayer API >= 28. [#1776](https://github.com/react-native-community/react-native-video/pull/1776)

### Version 5.1.0-alpha1
* Fixed Exoplayer doesn't work with mute=true (Android). [#1696](https://github.com/react-native-community/react-native-video/pull/1696)
* Added support for automaticallyWaitsToMinimizeStalling property (iOS) [#1723](https://github.com/react-native-community/react-native-video/pull/1723)
Expand Down
47 changes: 43 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ Video only:

```diff
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
+ `pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'`
+ `pod 'react-native-video', :podspec => '../node_modules/react-native-video/react-native-video.podspec'`
end
```

Video with caching ([more info](docs/caching.md)):

```diff
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
+ `pod 'react-native-video/VideoCaching', :path => '../node_modules/react-native-video/react-native-video.podspec'`
+ `pod 'react-native-video/VideoCaching', :podspec => '../node_modules/react-native-video/react-native-video.podspec'`
end
```

Expand Down Expand Up @@ -322,6 +322,7 @@ var styles = StyleSheet.create({
* [selectedAudioTrack](#selectedaudiotrack)
* [selectedTextTrack](#selectedtexttrack)
* [selectedVideoTrack](#selectedvideotrack)
* [showPictureInPictureOnLeave](#showpictureinpictureonleave)
* [source](#source)
* [stereoPan](#stereopan)
* [textTracks](#texttracks)
Expand Down Expand Up @@ -564,7 +565,38 @@ Determine whether the media should played as picture in picture.
* **false (default)** - Don't not play as picture in picture
* **true** - Play the media as picture in picture

Platforms: iOS
To use this feature on AndroidExoPlayer, you must:
* Add following to the AndroidManifest.xml -> MainActivity:
```
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
```
* [Enable PIP feature for you app](https://support.google.com/youtube/answer/7552722?co=GENIE.Platform%3DAndroid&hl=en) in your device settings
* Add following methods to MainActivity:
```
// To let JS knows about PIP mode change.
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
val intent = Intent("onPictureInPictureModeChanged")
intent.putExtra("isInPictureInPictureMode", isInPictureInPictureMode)
this.sendBroadcast(intent)
}

// To trigger PIP mode by pressing `Home` or `Recent` buttons
override fun onUserLeaveHint() {
val intent = Intent("onUserLeaveHint")
this.sendBroadcast(intent)
super.onUserLeaveHint()
}

// To close and kill the app when closing PIP window.
override fun onStop() {
super.onStop()
finishAndRemoveTask()
}
```
Platforms: iOS, Android ExoPlayer

#### playInBackground
Determine whether the media should continue playing while the app is in the background. This allows customers to continue listening to the audio.
Expand Down Expand Up @@ -733,6 +765,13 @@ If a track matching the specified Type (and Value if appropriate) is unavailable

Platforms: Android ExoPlayer

#### showPictureInPictureOnLeave
Determine whether player should enter picture in picture mode while pressing Back or Recent hardware button.
* **false (default)** - Don't not enter picture in picture on leave
* **true** - Enter picture in picture on leave

Platforms: Android ExoPlayer (when following [this](#pictureinpicture))

#### source
Sets the media source. You can pass an asset loaded via require or an object with a uri.

Expand Down Expand Up @@ -1031,7 +1070,7 @@ isActive: true
}
```

Platforms: iOS
Platforms: iOS, Android ExoPlayer (when following [this](#pictureinpicture) )

#### onPlaybackRateChange
Callback function that is called when the rate of playback changes - either paused or starts/resumes.
Expand Down
1 change: 1 addition & 0 deletions Video.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ Video.propTypes = {
fullscreenAutorotate: PropTypes.bool,
fullscreenOrientation: PropTypes.oneOf(['all','landscape','portrait']),
progressUpdateInterval: PropTypes.number,
showPictureInPictureOnLeave: PropTypes.bool,
useTextureView: PropTypes.bool,
hideShutterView: PropTypes.bool,
onLoadStart: PropTypes.func,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
Expand Down Expand Up @@ -89,6 +95,8 @@ class ReactExoplayerView extends FrameLayout implements
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}

private final BroadcastReceiver pipReceiver;
private final BroadcastReceiver leaveReceiver;
private final VideoEventEmitter eventEmitter;
private final ReactExoplayerConfig config;
private final DefaultBandwidthMeter bandwidthMeter;
Expand All @@ -110,6 +118,7 @@ class ReactExoplayerView extends FrameLayout implements
private boolean isInBackground;
private boolean isPaused;
private boolean isBuffering;
private boolean isInPictureInPictureMode;
private boolean muted = false;
private float rate = 1f;
private float audioVolume = 1f;
Expand Down Expand Up @@ -139,6 +148,7 @@ class ReactExoplayerView extends FrameLayout implements
private Map<String, String> requestHeaders;
private boolean mReportBandwidth = false;
private boolean controls;
private boolean showPictureInPictureOnLeave;
// \ End props

// React
Expand Down Expand Up @@ -179,6 +189,29 @@ public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig confi
themedReactContext.addLifecycleEventListener(this);
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);

ReactExoplayerView self = this;

pipReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
boolean isInPictureInPictureMode = intent.getBooleanExtra("isInPictureInPictureMode", false);
self.onPictureInPictureModeChanged(isInPictureInPictureMode);
}
};

leaveReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (showPictureInPictureOnLeave) {
self.setPictureInPicture(true);
}
}
};

Activity activity = themedReactContext.getCurrentActivity();
activity.registerReceiver(pipReceiver, new IntentFilter("onPictureInPictureModeChanged"));
matejpolak marked this conversation as resolved.
Show resolved Hide resolved
activity.registerReceiver(leaveReceiver, new IntentFilter("onUserLeaveHint"));

initializePlayer();
}

Expand Down Expand Up @@ -233,7 +266,7 @@ public void onHostResume() {
@Override
public void onHostPause() {
isInBackground = true;
if (playInBackground) {
if (playInBackground || showPictureInPictureOnLeave) {
return;
}
setPlayWhenReady(false);
Expand All @@ -242,6 +275,15 @@ public void onHostPause() {
@Override
public void onHostDestroy() {
stopPlayback();

Activity activity = themedReactContext.getCurrentActivity();
if (activity == null) return;
try {
activity.unregisterReceiver(pipReceiver);
activity.unregisterReceiver(leaveReceiver);
} catch (Exception ignore) {
// ignore if already unregistered
}
}

public void cleanUpResources() {
Expand Down Expand Up @@ -1212,4 +1254,49 @@ public void setControls(boolean controls) {
}
}
}

/**
* Handling showPictureInPictureOnLeave prop.
*
* @param showPictureInPictureOnLeaveProp If true, enter pip mode when pressing home or recent HW button.
*/
public void setShowPictureInPictureOnLeave(boolean showPictureInPictureOnLeaveProp) {
showPictureInPictureOnLeave = showPictureInPictureOnLeaveProp;
}

/**
* Handling pip prop.
*
* @param pictureInPicture Pip prop, if true, enter PIP mode.
*/
public void setPictureInPicture(boolean pictureInPicture) {
if (!isInPictureInPictureMode && pictureInPicture) {
this.enterPictureInPictureMode();
}
isInPictureInPictureMode = pictureInPicture;
}

/**
* PIP handled, for N devices that support it, not "officially".
*/
public void enterPictureInPictureMode() {
PackageManager packageManager = themedReactContext.getPackageManager();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& packageManager
.hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
long videoPosition = player.getCurrentPosition();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should be removed. The variable is not used anywhere and causes crashes.

Activity activity = themedReactContext.getCurrentActivity();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PictureInPictureParams.Builder params = new PictureInPictureParams.Builder();
activity.enterPictureInPictureMode(params.build());
} else {
activity.enterPictureInPictureMode();
}
}
}

public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
eventEmitter.pictureInPictureModeChanged(isInPictureInPictureMode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_SELECTED_VIDEO_TRACK_VALUE = "value";
private static final String PROP_HIDE_SHUTTER_VIEW = "hideShutterView";
private static final String PROP_CONTROLS = "controls";
private static final String PROP_PICTURE_IN_PICTURE = "pictureInPicture";
private static final String PROP_SHOW_PICTURE_IN_PICTURE_ON_LEAVE = "showPictureInPictureOnLeave";

private ReactExoplayerConfig config;

Expand Down Expand Up @@ -292,6 +294,16 @@ public void setBufferConfig(final ReactExoplayerView videoView, @Nullable Readab
}
}

@ReactProp(name = PROP_PICTURE_IN_PICTURE, defaultBoolean = false)
public void setPictureInPicture(final ReactExoplayerView videoView, final boolean pictureInPicture) {
videoView.setPictureInPicture(pictureInPicture);
}

@ReactProp(name = PROP_SHOW_PICTURE_IN_PICTURE_ON_LEAVE, defaultBoolean = false)
public void setShowPictureInPictureOnLeave(final ReactExoplayerView videoView, final boolean showPictureInPictureOnLeave) {
videoView.setShowPictureInPictureOnLeave(showPictureInPictureOnLeave);
}

private boolean startsWithValidScheme(String uriString) {
return uriString.startsWith("http://")
|| uriString.startsWith("https://")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class VideoEventEmitter {
private static final String EVENT_AUDIO_BECOMING_NOISY = "onVideoAudioBecomingNoisy";
private static final String EVENT_AUDIO_FOCUS_CHANGE = "onAudioFocusChanged";
private static final String EVENT_PLAYBACK_RATE_CHANGE = "onPlaybackRateChange";
private static final String EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED = "onPictureInPictureStatusChanged";

static final String[] Events = {
EVENT_LOAD_START,
Expand All @@ -68,6 +69,7 @@ class VideoEventEmitter {
EVENT_AUDIO_FOCUS_CHANGE,
EVENT_PLAYBACK_RATE_CHANGE,
EVENT_BANDWIDTH,
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED
};

@Retention(RetentionPolicy.SOURCE)
Expand All @@ -92,6 +94,7 @@ class VideoEventEmitter {
EVENT_AUDIO_FOCUS_CHANGE,
EVENT_PLAYBACK_RATE_CHANGE,
EVENT_BANDWIDTH,
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED
})
@interface VideoEvents {
}
Expand All @@ -118,6 +121,7 @@ class VideoEventEmitter {
private static final String EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus";
private static final String EVENT_PROP_IS_BUFFERING = "isBuffering";
private static final String EVENT_PROP_PLAYBACK_RATE = "playbackRate";
private static final String EVENT_PROP_PICTURE_IN_PICTURE_ACTIVE = "isActive";

private static final String EVENT_PROP_ERROR = "error";
private static final String EVENT_PROP_ERROR_STRING = "errorString";
Expand Down Expand Up @@ -278,6 +282,12 @@ void audioBecomingNoisy() {
receiveEvent(EVENT_AUDIO_BECOMING_NOISY, null);
}

void pictureInPictureModeChanged(boolean isInPictureInPictureMode) {
WritableMap map = Arguments.createMap();
map.putBoolean(EVENT_PROP_PICTURE_IN_PICTURE_ACTIVE, isInPictureInPictureMode);
receiveEvent(EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED, map);
}

private void receiveEvent(@VideoEvents String type, WritableMap event) {
eventEmitter.receiveEvent(viewId, type, event);
}
Expand Down