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

This commit is contained in:
olivier bouillet 2022-08-20 14:32:57 +02:00
commit 9e92f1ef3f
15 changed files with 255 additions and 37 deletions

View File

@ -10,10 +10,12 @@ assignees: ''
# Bug
<!--
Please provide a clear and concise description of what the bug is.
Include screenshots if needed.
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)
Before opening a ticket
* 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.
* 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

20
API.md
View File

@ -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|
@ -443,7 +444,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
@ -842,6 +843,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:

View File

@ -2,11 +2,20 @@
### Version 6.0.0-alpha.2
-
- 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)
### 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)

View File

@ -285,7 +285,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');
@ -490,6 +490,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,

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 android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.SurfaceView;
import android.view.TextureView;
@ -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;

View File

@ -205,18 +205,29 @@ class ReactExoplayerView extends FrameLayout implements
private final AudioManager audioManager;
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
// store last progress event values to avoid sending unnecessary messages
private long lastPos = -1;
private long lastBufferDuration = -1;
private long lastDuration = -1;
private final Handler progressHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PROGRESS:
if (player != null
&& player.getPlaybackState() == Player.STATE_READY
&& player.getPlayWhenReady()
) {
if (player != null) {
long pos = player.getCurrentPosition();
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos));
long duration = player.getDuration();
if (lastPos != pos
|| lastBufferDuration != bufferedDuration
|| lastDuration != duration) {
lastPos = pos;
lastBufferDuration = bufferedDuration;
lastDuration = duration;
eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos));
}
msg = obtainMessage(SHOW_PROGRESS);
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
}
@ -392,6 +403,15 @@ class ReactExoplayerView extends FrameLayout implements
eventListener = new Player.Listener() {
@Override
public void onPlaybackStateChanged(int playbackState) {
View playButton = playerControlView.findViewById(R.id.exo_play);
View pauseButton = playerControlView.findViewById(R.id.exo_pause);
if (playButton != null && playButton.getVisibility() == GONE) {
playButton.setVisibility(INVISIBLE);
}
if (pauseButton != null && pauseButton.getVisibility() == GONE) {
pauseButton.setVisibility(INVISIBLE);
}
reLayout(playPauseControlContainer);
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
player.removeListener(eventListener);
@ -804,7 +824,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);
}
}
}
@ -945,6 +968,7 @@ class ReactExoplayerView extends FrameLayout implements
int playbackState = player.getPlaybackState();
boolean playWhenReady = player.getPlayWhenReady();
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
eventEmitter.playbackRateChange(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
switch (playbackState) {
case Player.STATE_IDLE:
text += "idle";
@ -1005,9 +1029,15 @@ class ReactExoplayerView extends FrameLayout implements
private void videoLoaded() {
if (loadVideoStarted) {
loadVideoStarted = false;
setSelectedAudioTrack(audioTrackType, audioTrackValue);
setSelectedVideoTrack(videoTrackType, videoTrackValue);
setSelectedTextTrack(textTrackType, textTrackValue);
if (audioTrackType != null) {
setSelectedAudioTrack(audioTrackType, audioTrackValue);
}
if (videoTrackType != null) {
setSelectedVideoTrack(videoTrackType, videoTrackValue);
}
if (textTrackType != null) {
setSelectedTextTrack(textTrackType, textTrackValue);
}
Format videoFormat = player.getVideoFormat();
int width = videoFormat != null ? videoFormat.width : 0;
int height = videoFormat != null ? videoFormat.height : 0;
@ -1826,4 +1856,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_CONTROLS = "controls";
private static final String PROP_SUBTITLE_STYLE = "subtitleStyle";
private ReactExoplayerConfig config;
public ReactExoplayerViewManager(ReactExoplayerConfig config) {
@ -347,6 +349,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
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)
public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) {
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

View File

@ -12,11 +12,13 @@ enum RCTPlayerOperations {
static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) {
let type = criteria?.type
let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player)
let trackCount:Int! = player?.currentItem?.tracks.count ?? 0
// The first few tracks will be audio & video track
let firstTextIndex:Int = 0
for firstTextIndex in 0..<(player?.currentItem?.tracks.count ?? 0) {
if player?.currentItem?.tracks[firstTextIndex].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
var firstTextIndex:Int = 0
for i in 0..<(trackCount) {
if player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
firstTextIndex = i
break
}
}
@ -24,7 +26,8 @@ enum RCTPlayerOperations {
var selectedTrackIndex:Int = RCTVideoUnset
if (type == "disabled") {
// Do nothing. We want to ensure option is nil
// Select the last text index which is the disabled text track
selectedTrackIndex = trackCount - firstTextIndex
} else if (type == "language") {
let selectedValue = criteria?.value as? String
for i in 0..<textTracks.count {
@ -53,7 +56,7 @@ enum RCTPlayerOperations {
// in the situation that a selected text track is not available (eg. specifies a textTrack not available)
if (type != "disabled") && selectedTrackIndex == RCTVideoUnset {
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) as! CFArray
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user)
let captionSettings = captioningMediaCharacteristics as? [AnyHashable]
if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) {
selectedTrackIndex = 0 // If we can't find a match, use the first available track
@ -67,8 +70,8 @@ enum RCTPlayerOperations {
}
}
}
for i in firstTextIndex..<(player?.currentItem?.tracks.count ?? 0) {
for i in firstTextIndex..<(trackCount) {
var isEnabled = false
if selectedTrackIndex != RCTVideoUnset {
isEnabled = i == selectedTrackIndex + firstTextIndex
@ -175,7 +178,7 @@ enum RCTPlayerOperations {
return Promise<Bool>(on: .global()) { fulfill, reject in
guard CMTimeCompare(current, cmSeekTime) != 0 else {
reject(NSError())
reject(NSError(domain: "", code: 0, userInfo: nil))
return
}
if !paused { player.pause() }

View File

@ -1,5 +1,6 @@
import AVFoundation
import Promises
import Photos
/*!
* Collection of pure functions
@ -37,16 +38,17 @@ enum RCTVideoUtils {
return 0
}
static func urlFilePath(filepath:NSString!) -> NSURL! {
static func urlFilePath(filepath:NSString!, searchPath:FileManager.SearchPathDirectory) -> NSURL! {
if filepath.contains("file://") {
return NSURL(string: filepath as String)
}
// if no file found, check if the file exists in the Document directory
let paths:[String]! = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let paths:[String]! = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true)
var relativeFilePath:String! = filepath.lastPathComponent
// the file may be multiple levels below the documents directory
let fileComponents:[String]! = filepath.components(separatedBy: "Documents/")
let directoryString:String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents";
let fileComponents:[String]! = filepath.components(separatedBy: directoryString)
if fileComponents.count > 1 {
relativeFilePath = fileComponents[1]
}
@ -192,6 +194,7 @@ enum RCTVideoUtils {
static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] {
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
var validTextTracks:[TextTrack] = []
if let textTracks = textTracks, textTracks.count > 0 {
for i in 0..<textTracks.count {
var textURLAsset:AVURLAsset!
@ -199,7 +202,9 @@ enum RCTVideoUtils {
if textUri.lowercased().hasPrefix("http") {
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options:(assetOptions as! [String : Any]))
} else {
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?) as URL, options:nil)
let isDisabledTrack:Bool! = textTracks[i].type == "disabled"
let searchPath:FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory;
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, options:nil)
}
let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first
if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset
@ -215,9 +220,43 @@ enum RCTVideoUtils {
}
}
}
let emptyVttFile:TextTrack? = self.createEmptyVttFile()
if (emptyVttFile != nil) {
validTextTracks.append(emptyVttFile!)
}
return validTextTracks
}
/*
* Create an useless / almost empty VTT file in the list with available tracks. This track gets selected when you give type: "disabled" as the selectedTextTrack
* This is needed because there is a bug where sideloaded texttracks cannot be disabled in the AVPlayer. Loading this VTT file instead solves that problem.
* For more info see: https://github.com/react-native-community/react-native-video/issues/1144
*/
static func createEmptyVttFile() -> TextTrack? {
let fileManager = FileManager.default
let cachesDirectoryUrl = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let filePath = cachesDirectoryUrl.appendingPathComponent("empty.vtt").path
if !fileManager.fileExists(atPath: filePath) {
let stringToWrite = "WEBVTT\n\n1\n99:59:59.000 --> 99:59:59.001\n."
do {
try stringToWrite.write(to: URL(fileURLWithPath: filePath), atomically: true, encoding: String.Encoding.utf8)
} catch {
return nil
}
}
return TextTrack([
"language": "disabled",
"title": "EmptyVttFile",
"type": "text/vtt",
"uri": filePath,
])
}
static func delay(seconds: Int = 0) -> Promise<Void> {
return Promise<Void>(on: .global()) { fulfill, reject in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: {
@ -226,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?)? {
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

View File

@ -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)
}

View File

@ -5,7 +5,7 @@ import React
class RCTVideoManager: RCTViewManager {
override func view() -> UIView {
return RCTVideo(eventDispatcher: bridge.eventDispatcher())
return RCTVideo(eventDispatcher: bridge.eventDispatcher() as! RCTEventDispatcher)
}
func methodQueue() -> DispatchQueue {

View File

@ -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