diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 54296c23..223be5ae 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,10 +10,12 @@ assignees: '' # Bug ## Platform diff --git a/API.md b/API.md index e15ed199..08a95ae8 100644 --- a/API.md +++ b/API.md @@ -214,7 +214,7 @@ Follow the manual linking instuctions for React Native Windows 0.62 above, but s ## 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 ``` @@ -304,6 +304,7 @@ var styles = StyleSheet.create({ |[selectedTextTrack](#selectedtexttrack)|Android, iOS| |[selectedVideoTrack](#selectedvideotrack)|Android| |[source](#source)|All| +|[subtitleStyle](#subtitleStyle)|Android| |[textTracks](#texttracks)|Android, iOS| |[trackId](#trackId)|Android| |[useTextureView](#usetextureview)|Android| @@ -347,6 +348,13 @@ var styles = StyleSheet.create({ |[restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)|iOS| |[seek](#seek)|All| +### Static methods + +| Name |Plateforms Support | +|--|--| +|[getWidevineLevel](#getWidevineLevel)|Android| +|[isCodecSupported](#isCodecSupported)|Android| +|[isHEVCSupported](#isHEVCSupported)|Android| ### Configurable props @@ -426,9 +434,11 @@ Platforms: Android, iOS #### disableFocus 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 - + +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 #### disableDisconnectError @@ -439,7 +449,7 @@ Determines if the player needs to throw an error when connection is lost or not Platforms: Android ### DRM -To setup DRM please follow [this guide](./DRM.md) +To setup DRM please follow [this guide](./docs/DRM.md) 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://` +#### 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 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 +#### 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ccff1ce..a2bec6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,25 @@ ## 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 +- 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) - 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) - 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 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 - 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) - 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) diff --git a/Video.js b/Video.js index a0f47867..589b8fb4 100644 --- a/Video.js +++ b/Video.js @@ -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 { @@ -302,7 +303,7 @@ export default class Video extends Component { } 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; const RCTVideoInstance = this.getViewManagerConfig('RCTVideo'); @@ -510,6 +511,13 @@ Video.propTypes = { fullscreenAutorotate: PropTypes.bool, fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']), progressUpdateInterval: PropTypes.number, + subtitleStyle: PropTypes.shape({ + paddingTop: PropTypes.number, + paddingBottom: PropTypes.number, + paddingLeft: PropTypes.number, + paddingRight: PropTypes.number, + fontSize: PropTypes.number, + }), useTextureView: PropTypes.bool, useSecureView: PropTypes.bool, hideShutterView: PropTypes.bool, diff --git a/android/build.gradle b/android/build.gradle index a93e7599..310d97cd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -33,7 +33,7 @@ repositories { dependencies { 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' } @@ -41,8 +41,9 @@ dependencies { implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.core:core: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' } implementation 'com.squareup.okhttp3:okhttp:${OKHTTP_VERSION}' diff --git a/android/src/main/java/com/brentvatne/ReactBridgeUtils.java b/android/src/main/java/com/brentvatne/ReactBridgeUtils.java new file mode 100644 index 00000000..c3391283 --- /dev/null +++ b/android/src/main/java/com/brentvatne/ReactBridgeUtils.java @@ -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; + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index a6cd1c4f..462d610c 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -5,6 +5,7 @@ import android.content.Context; import androidx.core.content.ContextCompat; import android.util.AttributeSet; import android.util.Log; +import android.util.TypedValue; import android.view.Gravity; import android.view.SurfaceView; 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.ExoPlayer; 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.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.SubtitleView; @@ -100,6 +101,16 @@ public final class ExoPlayerView extends FrameLayout { 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() { View view; @@ -230,7 +241,7 @@ public final class ExoPlayerView extends FrameLayout { @Override public void onCues(List cues) { - subtitleLayout.onCues(cues); + subtitleLayout.setCues(cues); } // ExoPlayer.VideoListener implementation @@ -284,7 +295,7 @@ public final class ExoPlayerView extends FrameLayout { } @Override - public void onTracksInfoChanged(TracksInfo tracksInfo) { + public void onTracksChanged(Tracks tracks) { updateForCurrentTrackSelections(); } diff --git a/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.java new file mode 100644 index 00000000..f57bb964 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.java @@ -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; + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 1ba9bb6f..eb284e9d 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -18,6 +18,7 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import androidx.annotation.WorkerThread; +import androidx.activity.OnBackPressedCallback; import com.brentvatne.common.Track; 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.ExoPlayer; 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.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; @@ -129,6 +131,7 @@ class ReactExoplayerView extends FrameLayout implements private Player.Listener eventListener; private ExoPlayerView exoPlayerView; + private FullScreenPlayerView fullScreenPlayerView; private DataSource.Factory mediaDataSourceFactory; private ExoPlayer player; @@ -356,7 +359,6 @@ class ReactExoplayerView extends FrameLayout implements // Setting the player for the playerControlView playerControlView.setPlayer(player); - playerControlView.show(); playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container); // 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 eventListener = new Player.Listener() { @Override @@ -430,6 +436,7 @@ class ReactExoplayerView extends FrameLayout implements removeViewAt(indexOfPC); } addView(playerControlView, 1, layoutParams); + reLayout(playerControlView); } /** @@ -557,7 +564,7 @@ class ReactExoplayerView extends FrameLayout implements private void initializePlayerCore(ReactExoplayerView self) { ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - self.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + self.trackSelector = new DefaultTrackSelector(getContext(), videoTrackSelectionFactory); self.trackSelector.setParameters(trackSelector.buildUponParameters() .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); @@ -653,6 +660,12 @@ class ReactExoplayerView extends FrameLayout implements setControls(controls); applyModifiers(); 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 { @@ -707,6 +720,8 @@ class ReactExoplayerView extends FrameLayout implements return drmSessionManager; } }; + } else { + drmProvider = new DefaultDrmSessionManagerProvider(); } switch (type) { case C.TYPE_SS: @@ -813,7 +828,10 @@ class ReactExoplayerView extends FrameLayout implements player.setPlayWhenReady(true); } } 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 - public void onTracksInfoChanged(TracksInfo tracksInfo) { + public void onTracksChanged(Tracks tracks) { eventEmitter.textTracks(getTextTrackInfo()); eventEmitter.audioTracks(getAudioTrackInfo()); eventEmitter.videoTracks(getVideoTrackInfo()); - } @Override @@ -1605,10 +1622,10 @@ class ReactExoplayerView extends FrameLayout implements TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks); DefaultTrackSelector.Parameters selectionParameters = trackSelector.getParameters() - .buildUpon() - .setRendererDisabled(rendererIndex, false) - .setTrackSelectionOverrides(new TrackSelectionOverrides.Builder().addOverride(selectionOverride).build()) - .build(); + .buildUpon() + .setRendererDisabled(rendererIndex, false) + .addOverride(selectionOverride) + .build(); trackSelector.setParameters(selectionParameters); } @@ -1767,6 +1784,16 @@ class ReactExoplayerView extends FrameLayout implements if (activity == null) { return; } + + if (fullScreenPlayerView == null) { + fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, playerControlView, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + setFullscreen(false); + } + }); + } + Window window = activity.getWindow(); View decorView = window.getDecorView(); int uiOptions; @@ -1780,13 +1807,24 @@ class ReactExoplayerView extends FrameLayout implements | SYSTEM_UI_FLAG_FULLSCREEN; } eventEmitter.fullscreenWillPresent(); - decorView.setSystemUiVisibility(uiOptions); - eventEmitter.fullscreenDidPresent(); + post(() -> { + decorView.setSystemUiVisibility(uiOptions); + if (controls) { + fullScreenPlayerView.show(); + } + eventEmitter.fullscreenDidPresent(); + }); } else { uiOptions = View.SYSTEM_UI_FLAG_VISIBLE; eventEmitter.fullscreenWillDismiss(); - decorView.setSystemUiVisibility(uiOptions); - eventEmitter.fullscreenDidDismiss(); + post(() -> { + 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); + } } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index b6a3d9cd..ccfb483c 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -79,6 +79,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager createNativeModules(ReactApplicationContext reactContext) { - return Collections.emptyList(); + return Collections.singletonList( + new VideoDecoderPropertiesModule(reactContext) + ); } // Deprecated RN 0.47 diff --git a/android/src/main/java/com/brentvatne/react/VideoDecoderPropertiesModule.java b/android/src/main/java/com/brentvatne/react/VideoDecoderPropertiesModule.java new file mode 100644 index 00000000..6622e7f9 --- /dev/null +++ b/android/src/main/java/com/brentvatne/react/VideoDecoderPropertiesModule.java @@ -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; + } + +} diff --git a/android/src/main/res/layout/exo_player_control_view.xml b/android/src/main/res/layout/exo_player_control_view.xml index becee6a9..19440912 100644 --- a/android/src/main/res/layout/exo_player_control_view.xml +++ b/android/src/main/res/layout/exo_player_control_view.xml @@ -71,6 +71,14 @@ android:paddingRight="4dp" android:includeFontPadding="false" android:textColor="#FFBEBEBE"/> + + diff --git a/android/src/main/res/values/styles.xml b/android/src/main/res/values/styles.xml new file mode 100644 index 00000000..3459d9d6 --- /dev/null +++ b/android/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/examples/basic/src/VideoPlayer.android.tsx b/examples/basic/src/VideoPlayer.android.tsx index d8aa3b0d..7dcb674d 100644 --- a/examples/basic/src/VideoPlayer.android.tsx +++ b/examples/basic/src/VideoPlayer.android.tsx @@ -16,7 +16,7 @@ import { 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 { @@ -77,6 +77,28 @@ class VideoPlayer extends Component { video: Video; 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) => { this.setState({ duration: data.duration, loading: false, }); this.onAudioTracks(data) @@ -288,6 +310,18 @@ class VideoPlayer extends Component { ) } + renderInfoControl() { + return ( + { + this.popupInfo() + }} + > + {'decoderInfo'} + + ) + } + renderFullScreenControl() { return ( + + {this.renderInfoControl()} + {this.renderPause()} {this.renderRepeatModeControl()} diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift index d8ec8677..50df8e3f 100644 --- a/ios/Video/Features/RCTVideoUtils.swift +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -1,5 +1,6 @@ import AVFoundation import Promises +import Photos /*! * Collection of pure functions @@ -264,8 +265,23 @@ enum RCTVideoUtils { } } + static func preparePHAsset(uri: String) -> Promise { + return Promise(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?)? { - guard source.uri != nil && source.uri != "" else { return nil } + guard let sourceUri = source.uri, sourceUri != "" else { return nil } var asset:AVURLAsset! let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? "" let url = source.isNetwork || source.isAsset diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 59973a36..f482ff7c 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -227,10 +227,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH RCTVideoUtils.delay() .then{ [weak self] in guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)} - guard let source = self._source, - let assetResult = RCTVideoUtils.prepareAsset(source: source), - let asset = assetResult.asset, - let assetOptions = assetResult.assetOptions else { + guard let source = self._source else { + DebugLog("The source not exist") + throw NSError(domain: "", code: 0, userInfo: nil) + } + 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)'") throw NSError(domain: "", code: 0, userInfo: nil) } @@ -264,7 +274,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH 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.applyModifiers() self._player?.actionAtItemEnd = .none diff --git a/ios/Video/RCTVideoPlayerViewController.swift b/ios/Video/RCTVideoPlayerViewController.swift index 95c926af..e398e62f 100644 --- a/ios/Video/RCTVideoPlayerViewController.swift +++ b/ios/Video/RCTVideoPlayerViewController.swift @@ -19,8 +19,11 @@ class RCTVideoPlayerViewController: AVPlayerViewController { override func viewDidDisappear(_ animated: Bool) { 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