Merge branch 'master' of https://github.com/react-native-video/react-native-video into feat/add_new_events_on_tracks_changed

# Conflicts:
#	android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
This commit is contained in:
olivier bouillet 2022-09-12 21:58:27 +02:00
commit 5aa4d6697d
19 changed files with 538 additions and 39 deletions

View File

@ -10,10 +10,12 @@ assignees: ''
# Bug # Bug
<!-- <!--
Please provide a clear and concise description of what the bug is. Before opening a ticket
Include screenshots if needed. * Ensure the issue has not been already reported
Please test using the latest release of the library, as maybe said bug has been already fixed. * Please test using the latest release of the library, as maybe said bug has been already fixed.
If the library has multiple install methods, describe installation method (e.g., pod, not pod, with jetifier etc) * Provide a clear and concise description of what the bug is.
* If the library has multiple install methods, describe installation method (e.g., pod, not pod, with jetifier etc)
* Include screenshots if needed.
--> -->
## Platform ## Platform

94
API.md
View File

@ -214,7 +214,7 @@ Follow the manual linking instuctions for React Native Windows 0.62 above, but s
## Examples ## Examples
Run `yarn xbasic install` before running any of the examples. Run `yarn xbasic install` in the root directory before running any of the examples.
### iOS Example ### iOS Example
``` ```
@ -304,6 +304,7 @@ var styles = StyleSheet.create({
|[selectedTextTrack](#selectedtexttrack)|Android, iOS| |[selectedTextTrack](#selectedtexttrack)|Android, iOS|
|[selectedVideoTrack](#selectedvideotrack)|Android| |[selectedVideoTrack](#selectedvideotrack)|Android|
|[source](#source)|All| |[source](#source)|All|
|[subtitleStyle](#subtitleStyle)|Android|
|[textTracks](#texttracks)|Android, iOS| |[textTracks](#texttracks)|Android, iOS|
|[trackId](#trackId)|Android| |[trackId](#trackId)|Android|
|[useTextureView](#usetextureview)|Android| |[useTextureView](#usetextureview)|Android|
@ -347,6 +348,13 @@ var styles = StyleSheet.create({
|[restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)|iOS| |[restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)|iOS|
|[seek](#seek)|All| |[seek](#seek)|All|
### Static methods
| Name |Plateforms Support |
|--|--|
|[getWidevineLevel](#getWidevineLevel)|Android|
|[isCodecSupported](#isCodecSupported)|Android|
|[isHEVCSupported](#isHEVCSupported)|Android|
### Configurable props ### Configurable props
@ -426,9 +434,11 @@ Platforms: Android, iOS
#### disableFocus #### disableFocus
Determines whether video audio should override background music/audio in Android devices. Determines whether video audio should override background music/audio in Android devices.
* ** false (default)** - Override background audio/music * **false (default)** - Override background audio/music
* **true** - Let background audio/music from other apps play * **true** - Let background audio/music from other apps play
Note: Allows multiple videos to play if set to `true`. If `false`, when one video is playing and another is started, the first video will be paused.
Platforms: Android Platforms: Android
#### disableDisconnectError #### disableDisconnectError
@ -439,7 +449,7 @@ Determines if the player needs to throw an error when connection is lost or not
Platforms: Android Platforms: Android
### DRM ### DRM
To setup DRM please follow [this guide](./DRM.md) To setup DRM please follow [this guide](./docs/DRM.md)
Platforms: Android, iOS Platforms: Android, iOS
@ -866,6 +876,23 @@ The following other types are supported on some platforms, but aren't fully docu
`content://, ms-appx://, ms-appdata://, assets-library://` `content://, ms-appx://, ms-appdata://, assets-library://`
#### subtitleStyle
Property | Description | Platforms
--- | --- | ---
fontSizeTrack | Adjust the font size of the subtitles. Default: font size of the device | Android
paddingTop | Adjust the top padding of the subtitles. Default: 0| Android
paddingBottom | Adjust the bottom padding of the subtitles. Default: 0| Android
paddingLeft | Adjust the left padding of the subtitles. Default: 0| Android
paddingRight | Adjust the right padding of the subtitles. Default: 0| Android
Example:
```
subtitleStyle={{ paddingBottom: 50, fontSize: 20 }}
```
#### textTracks #### textTracks
Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format: Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
@ -1427,8 +1454,67 @@ this.player.seek(120, 50); // Seek to 2 minutes with +/- 50 milliseconds accurac
Platforms: iOS Platforms: iOS
#### Static methods
### Video Decoding capabilities
A module embed in ReactNativeVideo allow to query device supported feature.
To use it include the module as following:
```javascript
import { VideoDecoderProperties } from '@ifs/react-native-video-enhanced'
```
Platforms: Android
#### getWidevineLevel
Indicates whether the widevine level supported by device.
Possible results:
- **0** - unable to determine widevine support (typically not supported)
- **1**, **2**, **3** - Widevine level supported
Platforms: Android
Example:
```
VideoDecoderProperties.getWidevineLevel().then((widevineLevel) => {
...
}
```
#### isCodecSupported
Indicates whether the provided codec is supported level supported by device.
parameters:
- **mimetype**: mime type of codec to query
- **width**, **height**: resolution to query
Possible results:
- **true** - codec supported
- **false** - codec is not supported
Example:
```
VideoDecoderProperties.isCodecSupported('video/avc', 1920, 1080).then(
...
}
```
Platforms: Android
#### isHEVCSupported
Helper which Indicates whether the provided HEVC/1920*1080 is supported level supported by device.
It uses isCodecSupported internally.
Example:
```
VideoDecoderProperties.isHEVCSupported().then((hevcSupported) => {
...
}
```
### iOS App Transport Security ### iOS App Transport Security

View File

@ -1,16 +1,25 @@
## Changelog ## Changelog
### Version 6.0.0-alpha3
- Upgrade ExoPlayer to 2.18.1 [#2846](https://github.com/react-native-video/react-native-video/pull/2846)
### Version 6.0.0-alpha.2 ### Version 6.0.0-alpha.2
- Feature add new APIs to query supported features of device decoder (widevine level & codec capabilities) on android [#2740](https://github.com/react-native-video/react-native-video/pull/2740)
- Feature add support of subtitle styling on android [#2759](https://github.com/react-native-video/react-native-video/pull/2759)
- Fix Android #2690 ensure onEnd is not sent twice [#2690](https://github.com/react-native-video/react-native-video/issues/2690)
- Fix Exoplayer progress not reported when paused [#2664](https://github.com/react-native-video/react-native-video/pull/2664) - Fix Exoplayer progress not reported when paused [#2664](https://github.com/react-native-video/react-native-video/pull/2664)
- Call playbackRateChange onPlay and onPause [#1493](https://github.com/react-native-video/react-native-video/pull/1493) - Call playbackRateChange onPlay and onPause [#1493](https://github.com/react-native-video/react-native-video/pull/1493)
- Fix being unable to disable sideloaded texttracks in the AVPlayer [#2679](https://github.com/react-native-video/react-native-video/pull/2679) - Fix being unable to disable sideloaded texttracks in the AVPlayer [#2679](https://github.com/react-native-video/react-native-video/pull/2679)
- Fixed crash when iOS seek method called reject on the promise [#2743](https://github.com/react-native-video/react-native-video/pull/2743) - Fixed crash when iOS seek method called reject on the promise [#2743](https://github.com/react-native-video/react-native-video/pull/2743)
- Fix maxBitRate property being ignored on Android [#2670](https://github.com/react-native-video/react-native-video/pull/2670) - Fix maxBitRate property being ignored on Android [#2670](https://github.com/react-native-video/react-native-video/pull/2670)
- Fix crash when the source is a cameraroll [#2639] (https://github.com/react-native-video/react-native-video/pull/2639)
- Fix IOS UI frame drop on loading video [#2848] (https://github.com/react-native-video/react-native-video/pull/2848)
### Version 6.0.0-alpha.1 ### Version 6.0.0-alpha.1
- Remove Android MediaPlayer support [#2724](https://github.com/react-native-video/react-native-video/pull/2724) - Remove Android MediaPlayer support [#2724](https://github.com/react-native-video/react-native-video/pull/2724)
**WARNING**: when switching from older version to V6, you need to remove all refrerences of android-exoplayer. This android-exoplayer folder has been renamed to android. Exoplayer is now the only player implementation supported.
- Replace Image.propTypes with ImagePropTypes. [#2718](https://github.com/react-native-video/react-native-video/pull/2718) - Replace Image.propTypes with ImagePropTypes. [#2718](https://github.com/react-native-video/react-native-video/pull/2718)
- Fix iOS build caused by type mismatch [#2720](https://github.com/react-native-video/react-native-video/pull/2720) - Fix iOS build caused by type mismatch [#2720](https://github.com/react-native-video/react-native-video/pull/2720)
- ERROR TypeError: undefined is not an object (evaluating '_reactNative.Image.propTypes.resizeMode') [#2714](https://github.com/react-native-video/react-native-video/pull/2714) - ERROR TypeError: undefined is not an object (evaluating '_reactNative.Image.propTypes.resizeMode') [#2714](https://github.com/react-native-video/react-native-video/pull/2714)

View File

@ -14,7 +14,8 @@ const styles = StyleSheet.create({
}, },
}); });
export { TextTrackType, FilterType, DRMType }; const { VideoDecoderProperties } = NativeModules
export { TextTrackType, FilterType, DRMType, VideoDecoderProperties }
export default class Video extends Component { export default class Video extends Component {
@ -302,7 +303,7 @@ export default class Video extends Component {
} }
const isNetwork = !!(uri && uri.match(/^https?:/)); const isNetwork = !!(uri && uri.match(/^https?:/));
const isAsset = !!(uri && uri.match(/^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/)); const isAsset = !!(uri && uri.match(/^(assets-library|ph|ipod-library|file|content|ms-appx|ms-appdata):/));
let nativeResizeMode; let nativeResizeMode;
const RCTVideoInstance = this.getViewManagerConfig('RCTVideo'); const RCTVideoInstance = this.getViewManagerConfig('RCTVideo');
@ -510,6 +511,13 @@ Video.propTypes = {
fullscreenAutorotate: PropTypes.bool, fullscreenAutorotate: PropTypes.bool,
fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']), fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']),
progressUpdateInterval: PropTypes.number, progressUpdateInterval: PropTypes.number,
subtitleStyle: PropTypes.shape({
paddingTop: PropTypes.number,
paddingBottom: PropTypes.number,
paddingLeft: PropTypes.number,
paddingRight: PropTypes.number,
fontSize: PropTypes.number,
}),
useTextureView: PropTypes.bool, useTextureView: PropTypes.bool,
useSecureView: PropTypes.bool, useSecureView: PropTypes.bool,
hideShutterView: PropTypes.bool, hideShutterView: PropTypes.bool,

View File

@ -33,7 +33,7 @@ repositories {
dependencies { dependencies {
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
implementation('com.google.android.exoplayer:exoplayer:2.17.1') { implementation('com.google.android.exoplayer:exoplayer:2.18.1') {
exclude group: 'com.android.support' exclude group: 'com.android.support'
} }
@ -41,8 +41,9 @@ dependencies {
implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.core:core:1.1.0" implementation "androidx.core:core:1.1.0"
implementation "androidx.media:media:1.1.0" implementation "androidx.media:media:1.1.0"
implementation 'androidx.activity:activity:1.4.0'
implementation('com.google.android.exoplayer:extension-okhttp:2.17.1') { implementation('com.google.android.exoplayer:extension-okhttp:2.18.1') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp' exclude group: 'com.squareup.okhttp3', module: 'okhttp'
} }
implementation 'com.squareup.okhttp3:okhttp:${OKHTTP_VERSION}' implementation 'com.squareup.okhttp3:okhttp:${OKHTTP_VERSION}'

View File

@ -0,0 +1,22 @@
package com.brentvatne;
import com.facebook.react.bridge.ReadableMap;
/*
* This file define static helpers to parse in an easier way input props
*/
public class ReactBridgeUtils {
/*
retrieve key from map as int. fallback is returned if not available
*/
static public int safeGetInt(ReadableMap map, String key, int fallback) {
return map != null && map.hasKey(key) && !map.isNull(key) ? map.getInt(key) : fallback;
}
/*
retrieve key from map as double. fallback is returned if not available
*/
static public double safeGetDouble(ReadableMap map, String key, double fallback) {
return map != null && map.hasKey(key) && !map.isNull(key) ? map.getDouble(key) : fallback;
}
}

View File

@ -5,6 +5,7 @@ import android.content.Context;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity; import android.view.Gravity;
import android.view.SurfaceView; import android.view.SurfaceView;
import android.view.TextureView; import android.view.TextureView;
@ -18,7 +19,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
@ -100,6 +101,16 @@ public final class ExoPlayerView extends FrameLayout {
player.setVideoSurfaceView((SurfaceView) surfaceView); player.setVideoSurfaceView((SurfaceView) surfaceView);
} }
} }
public void setSubtitleStyle(SubtitleStyle style) {
// ensure we reset subtile style before reapplying it
subtitleLayout.setUserDefaultStyle();
subtitleLayout.setUserDefaultTextSize();
if (style.getFontSize() > 0) {
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.getFontSize());
}
subtitleLayout.setPadding(style.getPaddingLeft(), style.getPaddingTop(), style.getPaddingRight(), style.getPaddingBottom());
}
private void updateSurfaceView() { private void updateSurfaceView() {
View view; View view;
@ -230,7 +241,7 @@ public final class ExoPlayerView extends FrameLayout {
@Override @Override
public void onCues(List<Cue> cues) { public void onCues(List<Cue> cues) {
subtitleLayout.onCues(cues); subtitleLayout.setCues(cues);
} }
// ExoPlayer.VideoListener implementation // ExoPlayer.VideoListener implementation
@ -284,7 +295,7 @@ public final class ExoPlayerView extends FrameLayout {
} }
@Override @Override
public void onTracksInfoChanged(TracksInfo tracksInfo) { public void onTracksChanged(Tracks tracks) {
updateForCurrentTrackSelections(); updateForCurrentTrackSelections();
} }

View File

@ -0,0 +1,80 @@
package com.brentvatne.exoplayer;
import android.app.Dialog;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import androidx.activity.OnBackPressedCallback;
import com.google.android.exoplayer2.ui.PlayerControlView;
public class FullScreenPlayerView extends Dialog {
private final PlayerControlView playerControlView;
private final ExoPlayerView exoPlayerView;
private ViewGroup parent;
private final FrameLayout containerView;
private final OnBackPressedCallback onBackPressedCallback;
public FullScreenPlayerView(Context context, ExoPlayerView exoPlayerView, PlayerControlView playerControlView, OnBackPressedCallback onBackPressedCallback) {
super(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
this.playerControlView = playerControlView;
this.exoPlayerView = exoPlayerView;
this.onBackPressedCallback = onBackPressedCallback;
containerView = new FrameLayout(context);
setContentView(containerView, generateDefaultLayoutParams());
}
@Override
public void onBackPressed() {
super.onBackPressed();
onBackPressedCallback.handleOnBackPressed();
}
@Override
protected void onStart() {
parent = (FrameLayout)(exoPlayerView.getParent());
parent.removeView(exoPlayerView);
containerView.addView(exoPlayerView, generateDefaultLayoutParams());
if (playerControlView != null) {
ImageButton imageButton = playerControlView.findViewById(com.brentvatne.react.R.id.exo_fullscreen);
imageButton.setImageResource(com.google.android.exoplayer2.ui.R.drawable.exo_icon_fullscreen_exit);
imageButton.setContentDescription(getContext().getString(com.google.android.exoplayer2.ui.R.string.exo_controls_fullscreen_exit_description));
parent.removeView(playerControlView);
containerView.addView(playerControlView, generateDefaultLayoutParams());
}
super.onStart();
}
@Override
protected void onStop() {
containerView.removeView(exoPlayerView);
parent.addView(exoPlayerView, generateDefaultLayoutParams());
if (playerControlView != null) {
ImageButton imageButton = playerControlView.findViewById(com.brentvatne.react.R.id.exo_fullscreen);
imageButton.setImageResource(com.google.android.exoplayer2.ui.R.drawable.exo_icon_fullscreen_enter);
imageButton.setContentDescription(getContext().getString(com.google.android.exoplayer2.ui.R.string.exo_controls_fullscreen_enter_description));
containerView.removeView(playerControlView);
parent.addView(playerControlView, generateDefaultLayoutParams());
}
parent.requestLayout();
parent = null;
super.onStop();
}
private FrameLayout.LayoutParams generateDefaultLayoutParams() {
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
);
layoutParams.setMargins(0, 0, 0, 0);
return layoutParams;
}
}

View File

@ -18,6 +18,7 @@ import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.activity.OnBackPressedCallback;
import com.brentvatne.common.Track; import com.brentvatne.common.Track;
import com.brentvatne.common.VideoTrack; import com.brentvatne.common.VideoTrack;
@ -39,8 +40,9 @@ import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
@ -129,6 +131,7 @@ class ReactExoplayerView extends FrameLayout implements
private Player.Listener eventListener; private Player.Listener eventListener;
private ExoPlayerView exoPlayerView; private ExoPlayerView exoPlayerView;
private FullScreenPlayerView fullScreenPlayerView;
private DataSource.Factory mediaDataSourceFactory; private DataSource.Factory mediaDataSourceFactory;
private ExoPlayer player; private ExoPlayer player;
@ -356,7 +359,6 @@ class ReactExoplayerView extends FrameLayout implements
// Setting the player for the playerControlView // Setting the player for the playerControlView
playerControlView.setPlayer(player); playerControlView.setPlayer(player);
playerControlView.show();
playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container); playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container);
// Invoking onClick event for exoplayerView // Invoking onClick event for exoplayerView
@ -388,6 +390,10 @@ class ReactExoplayerView extends FrameLayout implements
} }
}); });
//Handling the fullScreenButton click event
ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
fullScreenButton.setOnClickListener(v -> setFullscreen(!isFullscreen));
// Invoking onPlaybackStateChanged and onPlayWhenReadyChanged events for Player // Invoking onPlaybackStateChanged and onPlayWhenReadyChanged events for Player
eventListener = new Player.Listener() { eventListener = new Player.Listener() {
@Override @Override
@ -430,6 +436,7 @@ class ReactExoplayerView extends FrameLayout implements
removeViewAt(indexOfPC); removeViewAt(indexOfPC);
} }
addView(playerControlView, 1, layoutParams); addView(playerControlView, 1, layoutParams);
reLayout(playerControlView);
} }
/** /**
@ -557,7 +564,7 @@ class ReactExoplayerView extends FrameLayout implements
private void initializePlayerCore(ReactExoplayerView self) { private void initializePlayerCore(ReactExoplayerView self) {
ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
self.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); self.trackSelector = new DefaultTrackSelector(getContext(), videoTrackSelectionFactory);
self.trackSelector.setParameters(trackSelector.buildUponParameters() self.trackSelector.setParameters(trackSelector.buildUponParameters()
.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));
@ -653,6 +660,12 @@ class ReactExoplayerView extends FrameLayout implements
setControls(controls); setControls(controls);
applyModifiers(); applyModifiers();
startBufferCheckTimer(); startBufferCheckTimer();
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, playerControlView, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
setFullscreen(false);
}
});
} }
private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
@ -707,6 +720,8 @@ class ReactExoplayerView extends FrameLayout implements
return drmSessionManager; return drmSessionManager;
} }
}; };
} else {
drmProvider = new DefaultDrmSessionManagerProvider();
} }
switch (type) { switch (type) {
case C.TYPE_SS: case C.TYPE_SS:
@ -813,7 +828,10 @@ class ReactExoplayerView extends FrameLayout implements
player.setPlayWhenReady(true); player.setPlayWhenReady(true);
} }
} else { } else {
player.setPlayWhenReady(false); // ensure playback is not ENDED, else it will trigger another ended event
if (player.getPlaybackState() != Player.STATE_ENDED) {
player.setPlayWhenReady(false);
}
} }
} }
@ -1290,11 +1308,10 @@ class ReactExoplayerView extends FrameLayout implements
} }
@Override @Override
public void onTracksInfoChanged(TracksInfo tracksInfo) { public void onTracksChanged(Tracks tracks) {
eventEmitter.textTracks(getTextTrackInfo()); eventEmitter.textTracks(getTextTrackInfo());
eventEmitter.audioTracks(getAudioTrackInfo()); eventEmitter.audioTracks(getAudioTrackInfo());
eventEmitter.videoTracks(getVideoTrackInfo()); eventEmitter.videoTracks(getVideoTrackInfo());
} }
@Override @Override
@ -1605,10 +1622,10 @@ class ReactExoplayerView extends FrameLayout implements
TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks); TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks);
DefaultTrackSelector.Parameters selectionParameters = trackSelector.getParameters() DefaultTrackSelector.Parameters selectionParameters = trackSelector.getParameters()
.buildUpon() .buildUpon()
.setRendererDisabled(rendererIndex, false) .setRendererDisabled(rendererIndex, false)
.setTrackSelectionOverrides(new TrackSelectionOverrides.Builder().addOverride(selectionOverride).build()) .addOverride(selectionOverride)
.build(); .build();
trackSelector.setParameters(selectionParameters); trackSelector.setParameters(selectionParameters);
} }
@ -1767,6 +1784,16 @@ class ReactExoplayerView extends FrameLayout implements
if (activity == null) { if (activity == null) {
return; return;
} }
if (fullScreenPlayerView == null) {
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, playerControlView, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
setFullscreen(false);
}
});
}
Window window = activity.getWindow(); Window window = activity.getWindow();
View decorView = window.getDecorView(); View decorView = window.getDecorView();
int uiOptions; int uiOptions;
@ -1780,13 +1807,24 @@ class ReactExoplayerView extends FrameLayout implements
| SYSTEM_UI_FLAG_FULLSCREEN; | SYSTEM_UI_FLAG_FULLSCREEN;
} }
eventEmitter.fullscreenWillPresent(); eventEmitter.fullscreenWillPresent();
decorView.setSystemUiVisibility(uiOptions); post(() -> {
eventEmitter.fullscreenDidPresent(); decorView.setSystemUiVisibility(uiOptions);
if (controls) {
fullScreenPlayerView.show();
}
eventEmitter.fullscreenDidPresent();
});
} else { } else {
uiOptions = View.SYSTEM_UI_FLAG_VISIBLE; uiOptions = View.SYSTEM_UI_FLAG_VISIBLE;
eventEmitter.fullscreenWillDismiss(); eventEmitter.fullscreenWillDismiss();
decorView.setSystemUiVisibility(uiOptions); post(() -> {
eventEmitter.fullscreenDidDismiss(); decorView.setSystemUiVisibility(uiOptions);
if (controls) {
fullScreenPlayerView.dismiss();
reLayout(exoPlayerView);
}
eventEmitter.fullscreenDidDismiss();
});
} }
} }
@ -1866,4 +1904,8 @@ class ReactExoplayerView extends FrameLayout implements
} }
} }
} }
public void setSubtitleStyle(SubtitleStyle style) {
exoPlayerView.setSubtitleStyle(style);
}
} }

View File

@ -79,6 +79,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_HIDE_SHUTTER_VIEW = "hideShutterView"; private static final String PROP_HIDE_SHUTTER_VIEW = "hideShutterView";
private static final String PROP_CONTROLS = "controls"; private static final String PROP_CONTROLS = "controls";
private static final String PROP_SUBTITLE_STYLE = "subtitleStyle";
private ReactExoplayerConfig config; private ReactExoplayerConfig config;
public ReactExoplayerViewManager(ReactExoplayerConfig config) { public ReactExoplayerViewManager(ReactExoplayerConfig config) {
@ -347,6 +349,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
videoView.setControls(controls); videoView.setControls(controls);
} }
@ReactProp(name = PROP_SUBTITLE_STYLE)
public void setSubtitleStyle(final ReactExoplayerView videoView, @Nullable final ReadableMap src) {
videoView.setSubtitleStyle(SubtitleStyle.parse(src));
}
@ReactProp(name = PROP_BUFFER_CONFIG) @ReactProp(name = PROP_BUFFER_CONFIG)
public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) { public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) {
int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;

View File

@ -0,0 +1,38 @@
package com.brentvatne.exoplayer;
import com.brentvatne.ReactBridgeUtils;
import com.facebook.react.bridge.ReadableMap;
/**
* Helper file to parse SubtitleStyle prop and build a dedicated class
*/
public class SubtitleStyle {
private static final String PROP_FONT_SIZE_TRACK = "fontSize";
private static final String PROP_PADDING_BOTTOM = "paddingBottom";
private static final String PROP_PADDING_TOP = "paddingTop";
private static final String PROP_PADDING_LEFT = "paddingLeft";
private static final String PROP_PADDING_RIGHT = "paddingRight";
private int fontSize = -1;
private int paddingLeft = 0;
private int paddingRight = 0;
private int paddingTop = 0;
private int paddingBottom = 0;
private SubtitleStyle() {}
int getFontSize() {return fontSize;}
int getPaddingBottom() {return paddingBottom;}
int getPaddingTop() {return paddingTop;}
int getPaddingLeft() {return paddingLeft;}
int getPaddingRight() {return paddingRight;}
public static SubtitleStyle parse(ReadableMap src) {
SubtitleStyle subtitleStyle = new SubtitleStyle();
subtitleStyle.fontSize = ReactBridgeUtils.safeGetInt(src, PROP_FONT_SIZE_TRACK, -1);
subtitleStyle.paddingBottom = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_BOTTOM, 0);
subtitleStyle.paddingTop = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_TOP, 0);
subtitleStyle.paddingLeft = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_LEFT, 0);
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0);
return subtitleStyle;
}
}

View File

@ -25,7 +25,9 @@ public class ReactVideoPackage implements ReactPackage {
@Override @Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList(); return Collections.singletonList(
new VideoDecoderPropertiesModule(reactContext)
);
} }
// Deprecated RN 0.47 // Deprecated RN 0.47

View File

@ -0,0 +1,107 @@
package com.brentvatne.react;
import android.annotation.SuppressLint;
import android.media.MediaCodecList;
import android.media.MediaDrm;
import android.media.MediaFormat;
import android.media.UnsupportedSchemeException;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.UUID;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class VideoDecoderPropertiesModule extends ReactContextBaseJavaModule {
ReactApplicationContext reactContext;
@NonNull
@Override
public String getName() {
return "VideoDecoderProperties";
}
@SuppressLint("ObsoleteSdkInt")
@ReactMethod
public void getWidevineLevel(Promise p) {
int widevineLevel = 0;
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
p.resolve(widevineLevel);
return;
}
final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
final String WIDEVINE_SECURITY_LEVEL_1 = "L1";
final String WIDEVINE_SECURITY_LEVEL_2 = "L2";
final String WIDEVINE_SECURITY_LEVEL_3 = "L3";
final String SECURITY_LEVEL_PROPERTY = "securityLevel";
String securityProperty = null;
try {
MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID);
securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY);
} catch (UnsupportedSchemeException e) {
e.printStackTrace();
}
if (securityProperty == null) {
p.resolve(widevineLevel);
return;
}
switch (securityProperty) {
case WIDEVINE_SECURITY_LEVEL_1: {
widevineLevel = 1;
break;
}
case WIDEVINE_SECURITY_LEVEL_2: {
widevineLevel = 2;
break;
}
case WIDEVINE_SECURITY_LEVEL_3: {
widevineLevel = 3;
break;
}
default: {
// widevineLevel 0
break;
}
}
p.resolve(widevineLevel);
}
@SuppressLint("ObsoleteSdkInt")
@ReactMethod
public void isCodecSupported(String mimeType, int width, int height, Promise p) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
p.resolve(false);
return;
}
MediaCodecList mRegularCodecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
String codecName = mRegularCodecs.findDecoderForFormat(format);
if (codecName == null) {
p.resolve(false);
} else {
p.resolve(true);
}
}
@ReactMethod
public void isHEVCSupported(Promise p) {
isCodecSupported("video/hevc", 1920, 1080, p);
}
public VideoDecoderPropertiesModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
}

View File

@ -71,6 +71,14 @@
android:paddingRight="4dp" android:paddingRight="4dp"
android:includeFontPadding="false" android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/> android:textColor="#FFBEBEBE"/>
<ImageButton
android:id="@id/exo_fullscreen"
style="@style/ExoMediaButton.FullScreen"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_margin="4dp"
android:scaleType="fitCenter" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ExoMediaButton.FullScreen">
<item name="android:src">@drawable/exo_icon_fullscreen_enter</item>
<item name="android:contentDescription">@string/exo_controls_fullscreen_enter_description</item>
</style>
</resources>

View File

@ -16,7 +16,7 @@ import {
import { Picker } from '@react-native-picker/picker' import { Picker } from '@react-native-picker/picker'
import Video, { TextTrackType } from 'react-native-video'; import Video, { VideoDecoderProperties, TextTrackType } from 'react-native-video';
class VideoPlayer extends Component { class VideoPlayer extends Component {
@ -77,6 +77,28 @@ class VideoPlayer extends Component {
video: Video; video: Video;
seekPanResponder: PanResponder | undefined; seekPanResponder: PanResponder | undefined;
popupInfo = () => {
VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => {
VideoDecoderProperties.isHEVCSupported().then((hevcSupported: boolean) => {
VideoDecoderProperties.isCodecSupported('video/avc', 1920, 1080).then(
(avcSupported: boolean) => {
this.toast(
true,
'Widevine level: ' +
widevineLevel +
'\n hevc: ' +
(hevcSupported ? '' : 'NOT') +
'supported' +
'\n avc: ' +
(avcSupported ? '' : 'NOT') +
'supported',
)
},
)
})
})
}
onLoad = (data: any) => { onLoad = (data: any) => {
this.setState({ duration: data.duration, loading: false, }); this.setState({ duration: data.duration, loading: false, });
this.onAudioTracks(data) this.onAudioTracks(data)
@ -288,6 +310,18 @@ class VideoPlayer extends Component {
) )
} }
renderInfoControl() {
return (
<TouchableOpacity
onPress={() => {
this.popupInfo()
}}
>
<Text style={[styles.controlOption]}>{'decoderInfo'}</Text>
</TouchableOpacity>
)
}
renderFullScreenControl() { renderFullScreenControl() {
return ( return (
<TouchableOpacity <TouchableOpacity
@ -541,6 +575,9 @@ class VideoPlayer extends Component {
</View> </View>
<View style={styles.bottomControls}> <View style={styles.bottomControls}>
<View style={styles.generalControls}> <View style={styles.generalControls}>
<View style={styles.generalControls}>
<View style={styles.resizeModeControl}>{this.renderInfoControl()}</View>
</View>
<View style={styles.resizeModeControl}>{this.renderPause()}</View> <View style={styles.resizeModeControl}>{this.renderPause()}</View>
<View style={styles.resizeModeControl}> <View style={styles.resizeModeControl}>
{this.renderRepeatModeControl()} {this.renderRepeatModeControl()}

View File

@ -1,5 +1,6 @@
import AVFoundation import AVFoundation
import Promises import Promises
import Photos
/*! /*!
* Collection of pure functions * Collection of pure functions
@ -264,8 +265,23 @@ enum RCTVideoUtils {
} }
} }
static func preparePHAsset(uri: String) -> Promise<AVAsset?> {
return Promise<AVAsset?>(on: .global()) { fulfill, reject in
let assetId = String(uri[uri.index(uri.startIndex, offsetBy: "ph://".count)...])
guard let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject else {
reject(NSError(domain: "", code: 0, userInfo: nil))
return
}
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
PHCachingImageManager().requestAVAsset(forVideo: phAsset, options: options) { data, _, _ in
fulfill(data)
}
}
}
static func prepareAsset(source:VideoSource) -> (asset:AVURLAsset?, assetOptions:NSMutableDictionary?)? { static func prepareAsset(source:VideoSource) -> (asset:AVURLAsset?, assetOptions:NSMutableDictionary?)? {
guard source.uri != nil && source.uri != "" else { return nil } guard let sourceUri = source.uri, sourceUri != "" else { return nil }
var asset:AVURLAsset! var asset:AVURLAsset!
let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? "" let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? ""
let url = source.isNetwork || source.isAsset let url = source.isNetwork || source.isAsset

View File

@ -227,10 +227,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
RCTVideoUtils.delay() RCTVideoUtils.delay()
.then{ [weak self] in .then{ [weak self] in
guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)} guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)}
guard let source = self._source, guard let source = self._source else {
let assetResult = RCTVideoUtils.prepareAsset(source: source), DebugLog("The source not exist")
let asset = assetResult.asset, throw NSError(domain: "", code: 0, userInfo: nil)
let assetOptions = assetResult.assetOptions else { }
if let uri = source.uri, uri.starts(with: "ph://") {
return Promise {
RCTVideoUtils.preparePHAsset(uri: uri).then { asset in
return self.playerItemPrepareText(asset:asset, assetOptions:nil)
}
}
}
guard let assetResult = RCTVideoUtils.prepareAsset(source: source),
let asset = assetResult.asset,
let assetOptions = assetResult.assetOptions else {
DebugLog("Could not find video URL in source '\(self._source)'") DebugLog("Could not find video URL in source '\(self._source)'")
throw NSError(domain: "", code: 0, userInfo: nil) throw NSError(domain: "", code: 0, userInfo: nil)
} }
@ -264,7 +274,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
self._playerItem?.preferredPeakBitRate = Double(maxBitRate) self._playerItem?.preferredPeakBitRate = Double(maxBitRate)
} }
self._player = AVPlayer(playerItem: self._playerItem) self._player = AVPlayer()
DispatchQueue.global(qos: .default).async {
self._player?.replaceCurrentItem(with: playerItem)
}
self._playerObserver.player = self._player self._playerObserver.player = self._player
self.applyModifiers() self.applyModifiers()
self._player?.actionAtItemEnd = .none self._player?.actionAtItemEnd = .none

View File

@ -19,8 +19,11 @@ class RCTVideoPlayerViewController: AVPlayerViewController {
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
rctDelegate.videoPlayerViewControllerWillDismiss(playerViewController: self)
rctDelegate.videoPlayerViewControllerDidDismiss(playerViewController: self) if rctDelegate != nil {
rctDelegate.videoPlayerViewControllerWillDismiss(playerViewController: self)
rctDelegate.videoPlayerViewControllerDidDismiss(playerViewController: self)
}
} }
#if !TARGET_OS_TV #if !TARGET_OS_TV