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:
commit
9e92f1ef3f
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
|
||||||
|
20
API.md
20
API.md
@ -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|
|
||||||
@ -443,7 +444,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
|
||||||
|
|
||||||
@ -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://`
|
`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:
|
||||||
|
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -2,11 +2,20 @@
|
|||||||
|
|
||||||
### Version 6.0.0-alpha.2
|
### 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
|
### 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)
|
||||||
|
9
Video.js
9
Video.js
@ -285,7 +285,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');
|
||||||
@ -490,6 +490,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,
|
||||||
|
22
android/src/main/java/com/brentvatne/ReactBridgeUtils.java
Normal file
22
android/src/main/java/com/brentvatne/ReactBridgeUtils.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
@ -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;
|
||||||
|
@ -205,18 +205,29 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
private final AudioManager audioManager;
|
private final AudioManager audioManager;
|
||||||
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
|
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()) {
|
private final Handler progressHandler = new Handler(Looper.getMainLooper()) {
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message msg) {
|
public void handleMessage(Message msg) {
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case SHOW_PROGRESS:
|
case SHOW_PROGRESS:
|
||||||
if (player != null
|
if (player != null) {
|
||||||
&& player.getPlaybackState() == Player.STATE_READY
|
|
||||||
&& player.getPlayWhenReady()
|
|
||||||
) {
|
|
||||||
long pos = player.getCurrentPosition();
|
long pos = player.getCurrentPosition();
|
||||||
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
|
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);
|
msg = obtainMessage(SHOW_PROGRESS);
|
||||||
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
|
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
|
||||||
}
|
}
|
||||||
@ -392,6 +403,15 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
eventListener = new Player.Listener() {
|
eventListener = new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackStateChanged(int playbackState) {
|
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);
|
reLayout(playPauseControlContainer);
|
||||||
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
|
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
|
||||||
player.removeListener(eventListener);
|
player.removeListener(eventListener);
|
||||||
@ -804,7 +824,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -945,6 +968,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
int playbackState = player.getPlaybackState();
|
int playbackState = player.getPlaybackState();
|
||||||
boolean playWhenReady = player.getPlayWhenReady();
|
boolean playWhenReady = player.getPlayWhenReady();
|
||||||
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
|
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
|
||||||
|
eventEmitter.playbackRateChange(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
|
||||||
switch (playbackState) {
|
switch (playbackState) {
|
||||||
case Player.STATE_IDLE:
|
case Player.STATE_IDLE:
|
||||||
text += "idle";
|
text += "idle";
|
||||||
@ -1005,9 +1029,15 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
private void videoLoaded() {
|
private void videoLoaded() {
|
||||||
if (loadVideoStarted) {
|
if (loadVideoStarted) {
|
||||||
loadVideoStarted = false;
|
loadVideoStarted = false;
|
||||||
setSelectedAudioTrack(audioTrackType, audioTrackValue);
|
if (audioTrackType != null) {
|
||||||
setSelectedVideoTrack(videoTrackType, videoTrackValue);
|
setSelectedAudioTrack(audioTrackType, audioTrackValue);
|
||||||
setSelectedTextTrack(textTrackType, textTrackValue);
|
}
|
||||||
|
if (videoTrackType != null) {
|
||||||
|
setSelectedVideoTrack(videoTrackType, videoTrackValue);
|
||||||
|
}
|
||||||
|
if (textTrackType != null) {
|
||||||
|
setSelectedTextTrack(textTrackType, textTrackValue);
|
||||||
|
}
|
||||||
Format videoFormat = player.getVideoFormat();
|
Format videoFormat = player.getVideoFormat();
|
||||||
int width = videoFormat != null ? videoFormat.width : 0;
|
int width = videoFormat != null ? videoFormat.width : 0;
|
||||||
int height = videoFormat != null ? videoFormat.height : 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,13 @@ enum RCTPlayerOperations {
|
|||||||
static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) {
|
static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) {
|
||||||
let type = criteria?.type
|
let type = criteria?.type
|
||||||
let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player)
|
let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player)
|
||||||
|
let trackCount:Int! = player?.currentItem?.tracks.count ?? 0
|
||||||
|
|
||||||
// The first few tracks will be audio & video track
|
// The first few tracks will be audio & video track
|
||||||
let firstTextIndex:Int = 0
|
var firstTextIndex:Int = 0
|
||||||
for firstTextIndex in 0..<(player?.currentItem?.tracks.count ?? 0) {
|
for i in 0..<(trackCount) {
|
||||||
if player?.currentItem?.tracks[firstTextIndex].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
|
if player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
|
||||||
|
firstTextIndex = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,7 +26,8 @@ enum RCTPlayerOperations {
|
|||||||
var selectedTrackIndex:Int = RCTVideoUnset
|
var selectedTrackIndex:Int = RCTVideoUnset
|
||||||
|
|
||||||
if (type == "disabled") {
|
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") {
|
} else if (type == "language") {
|
||||||
let selectedValue = criteria?.value as? String
|
let selectedValue = criteria?.value as? String
|
||||||
for i in 0..<textTracks.count {
|
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)
|
// in the situation that a selected text track is not available (eg. specifies a textTrack not available)
|
||||||
if (type != "disabled") && selectedTrackIndex == RCTVideoUnset {
|
if (type != "disabled") && selectedTrackIndex == RCTVideoUnset {
|
||||||
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) as! CFArray
|
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user)
|
||||||
let captionSettings = captioningMediaCharacteristics as? [AnyHashable]
|
let captionSettings = captioningMediaCharacteristics as? [AnyHashable]
|
||||||
if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) {
|
if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) {
|
||||||
selectedTrackIndex = 0 // If we can't find a match, use the first available track
|
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
|
var isEnabled = false
|
||||||
if selectedTrackIndex != RCTVideoUnset {
|
if selectedTrackIndex != RCTVideoUnset {
|
||||||
isEnabled = i == selectedTrackIndex + firstTextIndex
|
isEnabled = i == selectedTrackIndex + firstTextIndex
|
||||||
@ -175,7 +178,7 @@ enum RCTPlayerOperations {
|
|||||||
|
|
||||||
return Promise<Bool>(on: .global()) { fulfill, reject in
|
return Promise<Bool>(on: .global()) { fulfill, reject in
|
||||||
guard CMTimeCompare(current, cmSeekTime) != 0 else {
|
guard CMTimeCompare(current, cmSeekTime) != 0 else {
|
||||||
reject(NSError())
|
reject(NSError(domain: "", code: 0, userInfo: nil))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !paused { player.pause() }
|
if !paused { player.pause() }
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import Promises
|
import Promises
|
||||||
|
import Photos
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Collection of pure functions
|
* Collection of pure functions
|
||||||
@ -37,16 +38,17 @@ enum RCTVideoUtils {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static func urlFilePath(filepath:NSString!) -> NSURL! {
|
static func urlFilePath(filepath:NSString!, searchPath:FileManager.SearchPathDirectory) -> NSURL! {
|
||||||
if filepath.contains("file://") {
|
if filepath.contains("file://") {
|
||||||
return NSURL(string: filepath as String)
|
return NSURL(string: filepath as String)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no file found, check if the file exists in the Document directory
|
// 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
|
var relativeFilePath:String! = filepath.lastPathComponent
|
||||||
// the file may be multiple levels below the documents directory
|
// 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 {
|
if fileComponents.count > 1 {
|
||||||
relativeFilePath = fileComponents[1]
|
relativeFilePath = fileComponents[1]
|
||||||
}
|
}
|
||||||
@ -192,6 +194,7 @@ enum RCTVideoUtils {
|
|||||||
static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] {
|
static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] {
|
||||||
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
|
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
|
||||||
var validTextTracks:[TextTrack] = []
|
var validTextTracks:[TextTrack] = []
|
||||||
|
|
||||||
if let textTracks = textTracks, textTracks.count > 0 {
|
if let textTracks = textTracks, textTracks.count > 0 {
|
||||||
for i in 0..<textTracks.count {
|
for i in 0..<textTracks.count {
|
||||||
var textURLAsset:AVURLAsset!
|
var textURLAsset:AVURLAsset!
|
||||||
@ -199,7 +202,9 @@ enum RCTVideoUtils {
|
|||||||
if textUri.lowercased().hasPrefix("http") {
|
if textUri.lowercased().hasPrefix("http") {
|
||||||
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options:(assetOptions as! [String : Any]))
|
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options:(assetOptions as! [String : Any]))
|
||||||
} else {
|
} 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
|
let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first
|
||||||
if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset
|
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
|
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> {
|
static func delay(seconds: Int = 0) -> Promise<Void> {
|
||||||
return Promise<Void>(on: .global()) { fulfill, reject in
|
return Promise<Void>(on: .global()) { fulfill, reject in
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: {
|
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?)? {
|
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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import React
|
|||||||
class RCTVideoManager: RCTViewManager {
|
class RCTVideoManager: RCTViewManager {
|
||||||
|
|
||||||
override func view() -> UIView {
|
override func view() -> UIView {
|
||||||
return RCTVideo(eventDispatcher: bridge.eventDispatcher())
|
return RCTVideo(eventDispatcher: bridge.eventDispatcher() as! RCTEventDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
func methodQueue() -> DispatchQueue {
|
func methodQueue() -> DispatchQueue {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user