Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	ios/RCTVideo.m
#	package.json
This commit is contained in:
Ash Mishra 2018-07-06 16:01:02 -07:00
commit 05d4be2d9c
10 changed files with 384 additions and 202 deletions

View File

@ -1,5 +1,16 @@
## Changelog
### Version 3.0
* Inherit Android buildtools and SDK version from the root project [#1081](https://github.com/react-native-community/react-native-video/pull/1081)
* Automatically play on ExoPlayer when the paused prop is not set [#1083](https://github.com/react-native-community/react-native-video/pull/1083)
* Preserve Android MediaPlayer paused prop when backgrounding [#1082](https://github.com/react-native-community/react-native-video/pull/1082)
* Support specifying headers on ExoPlayer as part of the source [#805](https://github.com/react-native-community/react-native-video/pull/805)
* Prevent iOS onLoad event during seeking [#1088](https://github.com/react-native-community/react-native-video/pull/1088)
* ExoPlayer playableDuration incorrect [#1089](https://github.com/react-native-community/react-native-video/pull/1089)
### Version 2.3.1
* Revert PR to inherit Android SDK versions from root project. Re-add in 3.0 [#1080](https://github.com/react-native-community/react-native-video/pull/1080)
### Version 2.3.0
* Support allowsExternalPlayback on iOS [#1057](https://github.com/react-native-community/react-native-video/pull/1057)
* Inherit Android buildtools and SDK version from the root project [#999](https://github.com/react-native-community/react-native-video/pull/999)

117
README.md
View File

@ -5,10 +5,14 @@ A `<Video>` component for react-native, as seen in
Requires react-native >= 0.40.0, for RN support of 0.19.0 - 0.39.0 please use a pre 1.0 version.
### Version 3.0 breaking changes
Version 3.0 features a number of changes to existing behavior. See [Updating](#updating) for changes.
## TOC
* [Installation](#installation)
* [Usage](#usage)
* [Updating](#updating)
## Installation
@ -51,7 +55,7 @@ Note: you can also use the `ignoreSilentSwitch` prop, shown below.
Run `react-native link` to link the react-native-video library.
`react-native link` dont works properly with the tvOS target so we need to add the library manually.
`react-native link` doesnt work properly with the tvOS target so we need to add the library manually.
First select your project in Xcode.
@ -191,8 +195,6 @@ using System.Collections.Generic;
onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started
onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDismiss} // Callback after fullscreen stopped
onProgress={this.setTime} // Callback every ~250ms with currentTime
onTimedMetadata={this.onTimedMetadata} // Callback when the stream receive some metadata
style={styles.backgroundVideo} />
// Later to trigger fullscreen
@ -239,6 +241,8 @@ var styles = StyleSheet.create({
### Event props
* [onLoad](#onload)
* [onLoadStart](#onloadstart)
* [onProgress](#onprogress)
* [onTimedMetadata](#ontimedmetadata)
### Methods
* [seek](#seek)
@ -492,9 +496,9 @@ Payload:
Property | Description
--- | ---
isNetwork | Boolean indicating if the media is being loaded from the network
type | Type of the media. Not available on Windows
uri | URI for the media source. Not available on Windows
isNetwork | boolean | Boolean indicating if the media is being loaded from the network
type | string | Type of the media. Not available on Windows
uri | string | URI for the media source. Not available on Windows
Example:
```
@ -507,6 +511,46 @@ Example:
Platforms: all
#### onProgress
Callback function that is called every progressInterval seconds with info about which position the media is currently playing.
Property | Description
--- | ---
currentTime | number | Current position in seconds
playableDuration | number | Position to where the media can be played to using just the buffer in seconds
seekableDuration | number | Position to where the media can be seeked to in seconds. Typically, the total length of the media
Example:
```
{
currentTime: 5.2,
playableDuration: 34.6,
seekableDuration: 888
}
```
#### onTimedMetadata
Callback function that is called when timed metadata becomes available
Payload:
Property | Type | Description
--- | --- | ---
metadata | array | Array of metadata objects
Example:
```
{
metadata: [
{ value: 'Streaming Encoder', identifier: 'TRSN' },
{ value: 'Internet Stream', identifier: 'TRSO' },
{ value: 'Any Time You Like', identifier: 'TIT2' }
]
}
```
Platforms: Android ExoPlayer, iOS
### Methods
Methods operate on a ref to the Video element. You can create a ref using code like:
```
@ -558,14 +602,19 @@ For more detailed info check this [article](https://cocoacasts.com/how-to-add-ap
</details>
### Android Expansion File Usage
Within your render function, assuming you have a file called
"background.mp4" in your expansion file. Just add your main and (if applicable) patch version
Expansions files allow you to ship assets that exceed the 100MB apk size limit and don't need to be updated each time you push an app update.
This only supports mp4 files and they must not be compressed. Example command line for preventing compression:
```bash
zip -r -n .mp4 *.mp4 player.video.example.com
```
<Video
source={{uri: "background", mainVer: 1, patchVer: 0}}
/>
```
This will look for an .mp4 file (background.mp4) in the given expansion version.
```javascript
// Within your render function, assuming you have a file called
// "background.mp4" in your expansion file. Just add your main and (if applicable) patch version
<Video source={{uri: "background", mainVer: 1, patchVer: 0}} // Looks for .mp4 file (background.mp4) in the given expansion version.
resizeMode="cover" // Fill the whole screen at aspect ratio.
style={styles.backgroundVideo} />
### Load files with the RN Asset System
@ -598,9 +647,49 @@ To enable audio to play in background on iOS the audio session needs to be set t
- [Lumpen Radio](https://github.com/jhabdas/lumpen-radio) contains another example integration using local files and full screen background video.
## Updating
### Version 3.0
#### All platforms now auto-play
Previously, on Android ExoPlayer if the paused prop was not set, the media would not automatically start playing. The only way it would work was if you set `paused={false}`. This has been changed to automatically play if paused is not set so that the behavior is consistent across platforms.
#### All platforms now keep their paused state when returning from the background
Previously, on Android MediaPlayer if you setup an AppState event when the app went into the background and set a paused prop so that when you returned to the app the video would be paused it would be ignored.
Note, Windows does not have a concept of an app going into the background, so this doesn't apply there.
#### Use Android SDK 27 by default
Version 3.0 updates the Android build tools and SDK to version 27. React Native is in the process of [switchting over](https://github.com/facebook/react-native/issues/18095#issuecomment-395596130) to SDK 27 in preparation for Google's requirement that new Android apps [use SDK 26](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html) by August 2018.
You will either need to install the version 27 SDK and version 27.0.3 buildtools or modify your build.gradle file to configure react-native-video to use the same build settings as the rest of your app as described below.
##### Using app build settings
You will need to create a `project.ext` section in the top-level build.gradle file (not app/build.gradle). Fill in the values from the example below using the values found in your app/build.gradle file.
```
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
... // Various other settings go here
}
allprojects {
... // Various other settings go here
project.ext {
compileSdkVersion = 23
buildToolsVersion = "23.0.1"
minSdkVersion = 16
targetSdkVersion = 22
}
}
```
If you encounter an error `Could not find com.android.support:support-annotations:27.0.0.` reinstall your Android Support Repository.
## TODOS
- [ ] Add support for captions
- [ ] Add support for playing multiple videos in a sequence (will interfere with current `repeat` implementation)
- [x] Callback to get buffering progress for remote videos
- [ ] Bring API closer to HTML5 `<Video>` [reference](http://devdocs.io/html/element/video)

View File

@ -26,6 +26,29 @@ export default class Video extends Component {
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps);
}
toTypeString(x) {
switch (typeof x) {
case "object":
return x instanceof Date
? x.toISOString()
: JSON.stringify(x); // object, null
case "undefined":
return "";
default: // boolean, number, string
return x.toString();
}
}
stringsOnlyObject(obj) {
const strObj = {};
Object.keys(obj).forEach(x => {
strObj[x] = this.toTypeString(obj[x]);
});
return strObj;
}
seek = (time, tolerance = 100) => {
if (Platform.OS === 'ios') {
@ -202,6 +225,7 @@ export default class Video extends Component {
type: source.type || '',
mainVer: source.mainVer || 0,
patchVer: source.patchVer || 0,
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}
},
onVideoLoadStart: this._onLoadStart,
onVideoLoad: this._onLoad,

View File

@ -17,6 +17,8 @@ import com.google.android.exoplayer2.util.Util;
import okhttp3.Cookie;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import java.util.Map;
public class DataSourceUtil {
@ -49,9 +51,10 @@ public class DataSourceUtil {
DataSourceUtil.rawDataSourceFactory = factory;
}
public static DataSource.Factory getDefaultDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter) {
if (defaultDataSourceFactory == null) {
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter);
public static DataSource.Factory getDefaultDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
if (defaultDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) {
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter, requestHeaders);
}
return defaultDataSourceFactory;
}
@ -64,17 +67,21 @@ public class DataSourceUtil {
return new RawResourceDataSourceFactory(context.getApplicationContext());
}
private static DataSource.Factory buildDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter) {
private static DataSource.Factory buildDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
return new DefaultDataSourceFactory(context, bandwidthMeter,
buildHttpDataSourceFactory(context, bandwidthMeter));
buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders));
}
private static HttpDataSource.Factory buildHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter) {
private static HttpDataSource.Factory buildHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
OkHttpClient client = OkHttpClientProvider.getOkHttpClient();
CookieJarContainer container = (CookieJarContainer) client.cookieJar();
ForwardingCookieHandler handler = new ForwardingCookieHandler(context);
container.setCookieJar(new JavaNetCookieJar(handler));
return new OkHttpDataSourceFactory(client, getUserAgent(context), bandwidthMeter);
}
OkHttpDataSourceFactory okHttpDataSourceFactory = new OkHttpDataSourceFactory(client, getUserAgent(context), bandwidthMeter);
if (requestHeaders != null)
okHttpDataSourceFactory.getDefaultRequestProperties().set(requestHeaders);
return okHttpDataSourceFactory;
}
}

View File

@ -66,6 +66,7 @@ import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.lang.Math;
import java.util.Map;
import java.lang.Object;
import java.util.ArrayList;
@ -103,7 +104,7 @@ class ReactExoplayerView extends FrameLayout implements
private boolean loadVideoStarted;
private boolean isFullscreen;
private boolean isInBackground;
private boolean isPaused = true;
private boolean isPaused;
private boolean isBuffering;
private float rate = 1f;
@ -118,6 +119,7 @@ class ReactExoplayerView extends FrameLayout implements
private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false;
private boolean useTextureView = false;
private Map<String, String> requestHeaders;
// \ End props
// React
@ -135,7 +137,7 @@ class ReactExoplayerView extends FrameLayout implements
&& player.getPlayWhenReady()
) {
long pos = player.getCurrentPosition();
long bufferedDuration = player.getBufferedPercentage() * player.getDuration();
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration());
msg = obtainMessage(SHOW_PROGRESS);
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
@ -417,7 +419,7 @@ class ReactExoplayerView extends FrameLayout implements
* @return A new DataSource factory.
*/
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
return DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, useBandwidthMeter ? BANDWIDTH_METER : null);
return DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, useBandwidthMeter ? BANDWIDTH_METER : null, requestHeaders);
}
// AudioManager.OnAudioFocusChangeListener implementation
@ -661,14 +663,15 @@ class ReactExoplayerView extends FrameLayout implements
// ReactExoplayerViewManager public api
public void setSrc(final Uri uri, final String extension) {
public void setSrc(final Uri uri, final String extension, Map<String, String> headers) {
if (uri != null) {
boolean isOriginalSourceNull = srcUri == null;
boolean isSourceEqual = uri.equals(srcUri);
this.srcUri = uri;
this.extension = extension;
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, BANDWIDTH_METER);
this.requestHeaders = headers;
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, BANDWIDTH_METER, this.requestHeaders);
if (!isOriginalSourceNull && !isSourceEqual) {
reloadSource();

View File

@ -13,6 +13,7 @@ import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
@ -24,6 +25,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_SRC = "src";
private static final String PROP_SRC_URI = "uri";
private static final String PROP_SRC_TYPE = "type";
private static final String PROP_SRC_HEADERS = "requestHeaders";
private static final String PROP_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat";
private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack";
@ -80,6 +82,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
Context context = videoView.getContext().getApplicationContext();
String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null;
String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null;
Map<String, String> headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null;
if (TextUtils.isEmpty(uriString)) {
return;
@ -89,7 +93,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
Uri srcUri = Uri.parse(uriString);
if (srcUri != null) {
videoView.setSrc(srcUri, extension);
videoView.setSrc(srcUri, extension, headers);
}
} else {
int identifier = context.getResources().getIdentifier(
@ -208,4 +212,28 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
}
return ResizeMode.RESIZE_MODE_FIT;
}
/**
* toStringMap converts a {@link ReadableMap} into a HashMap.
*
* @param readableMap The ReadableMap to be conveted.
* @return A HashMap containing the data that was in the ReadableMap.
* @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java'
*/
public static Map<String, String> toStringMap(@Nullable ReadableMap readableMap) {
if (readableMap == null)
return null;
com.facebook.react.bridge.ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
if (!iterator.hasNextKey())
return null;
Map<String, String> result = new HashMap<>();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
result.put(key, readableMap.getString(key));
}
return result;
}
}

View File

@ -16,6 +16,7 @@ import com.android.vending.expansion.zipfile.APKExpansionSupport;
import com.android.vending.expansion.zipfile.ZipResourceFile;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.events.RCTEventEmitter;
@ -30,6 +31,8 @@ import java.util.Map;
import java.lang.Math;
import java.math.BigDecimal;
import javax.annotation.Nullable;
@SuppressLint("ViewConstructor")
public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnPreparedListener, MediaPlayer
.OnErrorListener, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener, LifecycleEventListener, MediaController.MediaPlayerControl {
@ -89,6 +92,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
private String mSrcUriString = null;
private String mSrcType = "mp4";
private ReadableMap mRequestHeaders = null;
private boolean mSrcIsNetwork = false;
private boolean mSrcIsAsset = false;
private ScalableType mResizeMode = ScalableType.LEFT_TOP;
@ -101,8 +105,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
private float mRate = 1.0f;
private float mActiveRate = 1.0f;
private boolean mPlayInBackground = false;
private boolean mActiveStatePauseStatus = false;
private boolean mActiveStatePauseStatusInitialized = false;
private boolean mBackgroundPaused = false;
private int mMainVer = 0;
private int mPatchVer = 0;
@ -128,7 +131,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
@Override
public void run() {
if (mMediaPlayerValid && !isCompleted &&!mPaused) {
if (mMediaPlayerValid && !isCompleted && !mPaused && !mBackgroundPaused) {
WritableMap event = Arguments.createMap();
event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0);
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, mVideoBufferedDuration / 1000.0); //TODO:mBufferUpdateRunnable
@ -207,16 +210,17 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
}
}
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset) {
setSrc(uriString,type,isNetwork,isAsset,0,0);
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders) {
setSrc(uriString, type, isNetwork, isAsset, requestHeaders, 0, 0);
}
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final int expansionMainVersion, final int expansionPatchVersion) {
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders, final int expansionMainVersion, final int expansionPatchVersion) {
mSrcUriString = uriString;
mSrcType = type;
mSrcIsNetwork = isNetwork;
mSrcIsAsset = isAsset;
mRequestHeaders = requestHeaders;
mMainVer = expansionMainVersion;
mPatchVer = expansionPatchVersion;
@ -245,7 +249,15 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
headers.put("Cookie", cookie);
}
setDataSource(uriString);
if (mRequestHeaders != null) {
headers.putAll(toStringMap(mRequestHeaders));
}
/* According to https://github.com/react-native-community/react-native-video/pull/537
* there is an issue with this where it can cause a IOException.
* TODO: diagnose this exception and fix it
*/
setDataSource(mThemedReactContext, parsedUrl, headers);
} else if (isAsset) {
if (uriString.startsWith("content://")) {
Uri parsedUrl = Uri.parse(uriString);
@ -291,8 +303,13 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
}
WritableMap src = Arguments.createMap();
WritableMap wRequestHeaders = Arguments.createMap();
wRequestHeaders.merge(mRequestHeaders);
src.putString(ReactVideoViewManager.PROP_SRC_URI, uriString);
src.putString(ReactVideoViewManager.PROP_SRC_TYPE, type);
src.putMap(ReactVideoViewManager.PROP_SRC_HEADERS, wRequestHeaders);
src.putBoolean(ReactVideoViewManager.PROP_SRC_IS_NETWORK, isNetwork);
if(mMainVer>0) {
src.putInt(ReactVideoViewManager.PROP_SRC_MAINVER, mMainVer);
@ -334,11 +351,6 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
mPaused = paused;
if ( !mActiveStatePauseStatusInitialized ) {
mActiveStatePauseStatus = mPaused;
mActiveStatePauseStatusInitialized = true;
}
if (!mMediaPlayerValid) {
return;
}
@ -410,8 +422,16 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
if (mMediaPlayerValid) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!mPaused) { // Applying the rate while paused will cause the video to start
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate));
mActiveRate = rate;
/* Per https://stackoverflow.com/questions/39442522/setplaybackparams-causes-illegalstateexception
* Some devices throw an IllegalStateException if you set the rate without first calling reset()
* TODO: Call reset() then reinitialize the player
*/
try {
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate));
mActiveRate = rate;
} catch (Exception e) {
Log.e(ReactVideoViewManager.REACT_CLASS, "Unable to set rate, unsupported on this device");
}
}
} else {
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android versions below 6.0");
@ -579,39 +599,62 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
super.onAttachedToWindow();
if(mMainVer>0) {
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset,mMainVer,mPatchVer);
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders, mMainVer, mPatchVer);
}
else {
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset);
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders);
}
}
@Override
public void onHostPause() {
if (mMediaPlayer != null && !mPlayInBackground) {
mActiveStatePauseStatus = mPaused;
// Pause the video in background
setPausedModifier(true);
if (mMediaPlayerValid && !mPaused && !mPlayInBackground) {
/* Pause the video in background
* Don't update the paused prop, developers should be able to update it on background
* so that when you return to the app the video is paused
*/
mBackgroundPaused = true;
mMediaPlayer.pause();
}
}
@Override
public void onHostResume() {
if (mMediaPlayer != null && !mPlayInBackground) {
mBackgroundPaused = false;
if (mMediaPlayerValid && !mPlayInBackground && !mPaused) {
new Handler().post(new Runnable() {
@Override
public void run() {
// Restore original state
setPausedModifier(mActiveStatePauseStatus);
setPausedModifier(false);
}
});
}
}
@Override
public void onHostDestroy() {
}
/**
* toStringMap converts a {@link ReadableMap} into a HashMap.
*
* @param readableMap The ReadableMap to be conveted.
* @return A HashMap containing the data that was in the ReadableMap.
* @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java'
*/
public static Map<String, String> toStringMap(@Nullable ReadableMap readableMap) {
Map<String, String> result = new HashMap<>();
if (readableMap == null)
return result;
com.facebook.react.bridge.ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
result.put(key, readableMap.getString(key));
}
return result;
}
}

View File

@ -21,6 +21,7 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
public static final String PROP_SRC = "src";
public static final String PROP_SRC_URI = "uri";
public static final String PROP_SRC_TYPE = "type";
public static final String PROP_SRC_HEADERS = "requestHeaders";
public static final String PROP_SRC_IS_NETWORK = "isNetwork";
public static final String PROP_SRC_MAINVER = "mainVer";
public static final String PROP_SRC_PATCHVER = "patchVer";
@ -86,6 +87,7 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
src.getString(PROP_SRC_TYPE),
src.getBoolean(PROP_SRC_IS_NETWORK),
src.getBoolean(PROP_SRC_IS_ASSET),
src.getMap(PROP_SRC_HEADERS),
mainVer,
patchVer
);
@ -95,8 +97,9 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
src.getString(PROP_SRC_URI),
src.getString(PROP_SRC_TYPE),
src.getBoolean(PROP_SRC_IS_NETWORK),
src.getBoolean(PROP_SRC_IS_ASSET)
);
src.getBoolean(PROP_SRC_IS_ASSET),
src.getMap(PROP_SRC_HEADERS)
);
}
}

View File

@ -26,6 +26,7 @@ static NSString *const timedMetadata = @"timedMetadata";
/* Required to publish events */
RCTEventDispatcher *_eventDispatcher;
BOOL _playbackRateObserverRegistered;
BOOL _videoLoadStarted;
bool _pendingSeek;
float _pendingSeekTime;
@ -96,7 +97,7 @@ static NSString *const timedMetadata = @"timedMetadata";
- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem {
RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init];
playerLayer.showsPlaybackControls = NO;
playerLayer.showsPlaybackControls = YES;
playerLayer.rctDelegate = self;
playerLayer.view.frame = self.bounds;
playerLayer.player = player;
@ -324,6 +325,7 @@ static NSString *const timedMetadata = @"timedMetadata";
}
});
});
_videoLoadStarted = YES;
}
- (NSURL*) urlFilePath:(NSString*) filepath {
@ -350,14 +352,24 @@ static NSString *const timedMetadata = @"timedMetadata";
NSString *uri = [source objectForKey:@"uri"];
NSString *subtitleUri = _selectedTextTrack[@"uri"];
NSString *type = [source objectForKey:@"type"];
NSDictionary *headers = [source objectForKey:@"requestHeaders"];
AVURLAsset *asset;
AVURLAsset *subAsset;
if (isNetwork) {
NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc]init];
/* Per #1091, this is not a public API. We need to either get approval from Apple to use this
* or use a different approach.
if ([headers count] > 0) {
[assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"];
}
*/
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:@{AVURLAssetHTTPCookiesKey : cookies}];
subAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:subtitleUri] options:@{AVURLAssetHTTPCookiesKey : cookies}];
[assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey];
asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:assetOptions];
subAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:subtitleUri] options:assetOptions];
}
else if (isAsset) // assets on iOS have to be in the Documents folder
{
@ -404,66 +416,62 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == _playerItem) {
if (object == _playerItem) {
// When timeMetadata is read the event onTimedMetadata is triggered
if ([keyPath isEqualToString: timedMetadata])
{
NSArray<AVMetadataItem *> *items = [change objectForKey:@"new"];
if (items && ![items isEqual:[NSNull null]] && items.count > 0) {
NSMutableArray *array = [NSMutableArray new];
for (AVMetadataItem *item in items) {
NSString *value = item.value;
NSString *identifier = item.identifier;
if (![value isEqual: [NSNull null]]) {
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]];
[array addObject:dictionary];
}
}
self.onTimedMetadata(@{
@"target": self.reactTag,
@"metadata": array
});
if ([keyPath isEqualToString:timedMetadata]) {
NSArray<AVMetadataItem *> *items = [change objectForKey:@"new"];
if (items && ![items isEqual:[NSNull null]] && items.count > 0) {
NSMutableArray *array = [NSMutableArray new];
for (AVMetadataItem *item in items) {
NSString *value = item.value;
NSString *identifier = item.identifier;
if (![value isEqual: [NSNull null]]) {
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]];
[array addObject:dictionary];
}
}
self.onTimedMetadata(@{
@"target": self.reactTag,
@"metadata": array
});
}
}
if ([keyPath isEqualToString:statusKeyPath]) {
// Handle player item status change.
if (_playerItem.status == AVPlayerItemStatusReadyToPlay) {
float duration = CMTimeGetSeconds(_playerItem.asset.duration);
if (isnan(duration)) {
duration = 0.0;
}
NSObject *width = @"undefined";
NSObject *height = @"undefined";
NSString *orientation = @"undefined";
if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) {
AVAssetTrack *videoAsset = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
width = [NSNumber numberWithFloat:videoAsset.naturalSize.width];
height = [NSNumber numberWithFloat:videoAsset.naturalSize.height];
CGAffineTransform preferredTransform = [videoAsset preferredTransform];
if ((videoAsset.naturalSize.width == preferredTransform.tx
&& videoAsset.naturalSize.height == preferredTransform.ty)
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0))
AVAssetTrack *videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
width = [NSNumber numberWithFloat:videoTrack.naturalSize.width];
height = [NSNumber numberWithFloat:videoTrack.naturalSize.height];
CGAffineTransform preferredTransform = [videoTrack preferredTransform];
if ((videoTrack.naturalSize.width == preferredTransform.tx
&& videoTrack.naturalSize.height == preferredTransform.ty)
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0))
{
orientation = @"landscape";
} else
} else {
orientation = @"portrait";
}
}
if(self.onVideoLoad) {
if (self.onVideoLoad && _videoLoadStarted) {
self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration],
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)],
@"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse],
@ -473,21 +481,21 @@ static NSString *const timedMetadata = @"timedMetadata";
@"canStepBackward": [NSNumber numberWithBool:_playerItem.canStepBackward],
@"canStepForward": [NSNumber numberWithBool:_playerItem.canStepForward],
@"naturalSize": @{
@"width": width,
@"height": height,
@"orientation": orientation
},
@"width": width,
@"height": height,
@"orientation": orientation
},
@"textTracks": [self getTextTrackInfo],
@"target": self.reactTag});
}
}
_videoLoadStarted = NO;
[self attachListeners];
[self applyModifiers];
} else if(_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) {
} else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) {
self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code],
@"domain": _playerItem.error.domain},
@"target": self.reactTag});
@"target": self.reactTag});
}
} else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) {
_playerBufferEmpty = YES;
@ -500,28 +508,28 @@ static NSString *const timedMetadata = @"timedMetadata";
_playerBufferEmpty = NO;
self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag});
}
} else if (object == _playerLayer) {
if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) {
if([change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) {
self.onReadyForDisplay(@{@"target": self.reactTag});
}
} else if (object == _playerLayer) {
if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) {
if([change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) {
self.onReadyForDisplay(@{@"target": self.reactTag});
}
}
} else if (object == _player) {
if([keyPath isEqualToString:playbackRate]) {
if(self.onPlaybackRateChange) {
self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
if(_playbackStalled && _player.rate > 0) {
if(self.onPlaybackResume) {
self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
_playbackStalled = NO;
}
if([keyPath isEqualToString:playbackRate]) {
if(self.onPlaybackRateChange) {
self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
if(_playbackStalled && _player.rate > 0) {
if(self.onPlaybackResume) {
self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
_playbackStalled = NO;
}
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

View File

@ -1,83 +1,49 @@
{
"_args": [
[
"git://github.com/nfb-onf/react-native-video.git",
"/Users/amishra/Development/react_films"
]
],
"_from": "git://github.com/nfb-onf/react-native-video.git",
"_id": "react-native-video@git://github.com/nfb-onf/react-native-video.git#8ce39e5b82108e6b9ea8549bd72ba58e95f04647",
"_inBundle": false,
"_integrity": "",
"_location": "/react-native-video",
"_phantomChildren": {},
"_requested": {
"type": "git",
"raw": "git://github.com/nfb-onf/react-native-video.git",
"rawSpec": "git://github.com/nfb-onf/react-native-video.git",
"saveSpec": "git://github.com/nfb-onf/react-native-video.git",
"fetchSpec": "git://github.com/nfb-onf/react-native-video.git",
"gitCommittish": null
},
"_requiredBy": [
"/"
],
"_resolved": "git://github.com/nfb-onf/react-native-video.git#8ce39e5b82108e6b9ea8549bd72ba58e95f04647",
"_spec": "git://github.com/nfb-onf/react-native-video.git",
"_where": "/Users/amishra/Development/react_films",
"author": {
"name": "Brent Vatne",
"email": "brentvatne@gmail.com",
"url": "https://github.com/brentvatne"
},
"bugs": {
"url": "https://github.com/brentvatne/react-native-video/issues"
},
"contributors": [
{
"name": "Isaiah Grey",
"email": "isaiahgrey@gmail.com"
"name": "react-native-video",
"version": "3.0.0",
"description": "A <Video /> element for react-native",
"main": "Video.js",
"license": "MIT",
"author": "Brent Vatne <brentvatne@gmail.com> (https://github.com/brentvatne)",
"contributors": [
{
"name": "Isaiah Grey",
"email": "isaiahgrey@gmail.com"
},
{
"name": "Johannes Lumpe",
"email": "johannes@lum.pe"
},
{
"name": "Baris Sencan",
"email": "baris.sncn@gmail.com"
},
{
"name": "Hampton Maxwell",
"email": "me@hamptonmaxwell.com"
}
],
"repository": {
"type": "git",
"url": "git@github.com:brentvatne/react-native-video.git"
},
{
"name": "Johannes Lumpe",
"email": "johannes@lum.pe"
"devDependencies": {
"jest-cli": "0.2.1",
"eslint": "1.10.3",
"babel-eslint": "5.0.0-beta8",
"eslint-plugin-react": "3.16.1",
"eslint-config-airbnb": "4.0.0"
},
{
"name": "Baris Sencan",
"email": "baris.sncn@gmail.com"
"dependencies": {
"keymirror": "0.1.1",
"prop-types": "^15.5.10"
},
{
"name": "Hampton Maxwell",
"email": "me@hamptonmaxwell.com"
"scripts": {
"test": "node_modules/.bin/eslint *.js"
},
"rnpm": {
"android": {
"sourceDir": "./android-exoplayer"
}
}
],
"dependencies": {
"keymirror": "0.1.1",
"prop-types": "^15.5.10"
},
"description": "A <Video /> element for react-native",
"devDependencies": {
"babel-eslint": "5.0.0-beta8",
"eslint": "1.10.3",
"eslint-config-airbnb": "4.0.0",
"eslint-plugin-react": "3.16.1",
"jest-cli": "0.2.1"
},
"homepage": "https://github.com/brentvatne/react-native-video#readme",
"license": "MIT",
"main": "Video.js",
"name": "react-native-video",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/brentvatne/react-native-video.git"
},
"rnpm": {
"android": {
"sourceDir": "./android-exoplayer"
}
},
"scripts": {
"test": "eslint *.js"
},
"version": "2.2.0"
}