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 # 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
View File

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

View File

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

View File

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

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

View File

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

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

View File

@ -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
@ -68,7 +71,7 @@ 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() }

View File

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

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

View File

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

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