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 ## 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 ### Version 2.3.0
* Support allowsExternalPlayback on iOS [#1057](https://github.com/react-native-community/react-native-video/pull/1057) * 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) * 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. 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 ## TOC
* [Installation](#installation) * [Installation](#installation)
* [Usage](#usage) * [Usage](#usage)
* [Updating](#updating)
## Installation ## 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. 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. First select your project in Xcode.
@ -191,8 +195,6 @@ using System.Collections.Generic;
onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started
onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDismiss} // Callback after fullscreen stopped 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} /> style={styles.backgroundVideo} />
// Later to trigger fullscreen // Later to trigger fullscreen
@ -239,6 +241,8 @@ var styles = StyleSheet.create({
### Event props ### Event props
* [onLoad](#onload) * [onLoad](#onload)
* [onLoadStart](#onloadstart) * [onLoadStart](#onloadstart)
* [onProgress](#onprogress)
* [onTimedMetadata](#ontimedmetadata)
### Methods ### Methods
* [seek](#seek) * [seek](#seek)
@ -492,9 +496,9 @@ Payload:
Property | Description Property | Description
--- | --- --- | ---
isNetwork | Boolean indicating if the media is being loaded from the network isNetwork | boolean | Boolean indicating if the media is being loaded from the network
type | Type of the media. Not available on Windows type | string | Type of the media. Not available on Windows
uri | URI for the media source. Not available on Windows uri | string | URI for the media source. Not available on Windows
Example: Example:
``` ```
@ -507,6 +511,46 @@ Example:
Platforms: all 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
Methods operate on a ref to the Video element. You can create a ref using code like: 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> </details>
### Android Expansion File Usage ### Android Expansion File Usage
Within your render function, assuming you have a file called 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.
"background.mp4" in your expansion file. Just add your main and (if applicable) patch version
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}} ```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
This will look for an .mp4 file (background.mp4) in the given expansion 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 ### 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. - [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 ## TODOS
- [ ] Add support for captions
- [ ] Add support for playing multiple videos in a sequence (will interfere with current `repeat` implementation) - [ ] Add support for playing multiple videos in a sequence (will interfere with current `repeat` implementation)
- [x] Callback to get buffering progress for remote videos - [x] Callback to get buffering progress for remote videos
- [ ] Bring API closer to HTML5 `<Video>` [reference](http://devdocs.io/html/element/video) - [ ] 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) { setNativeProps(nativeProps) {
this._root.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) => { seek = (time, tolerance = 100) => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
@ -202,6 +225,7 @@ export default class Video extends Component {
type: source.type || '', type: source.type || '',
mainVer: source.mainVer || 0, mainVer: source.mainVer || 0,
patchVer: source.patchVer || 0, patchVer: source.patchVer || 0,
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}
}, },
onVideoLoadStart: this._onLoadStart, onVideoLoadStart: this._onLoadStart,
onVideoLoad: this._onLoad, onVideoLoad: this._onLoad,

View File

@ -17,6 +17,8 @@ import com.google.android.exoplayer2.util.Util;
import okhttp3.Cookie; import okhttp3.Cookie;
import okhttp3.JavaNetCookieJar; import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import java.util.Map;
public class DataSourceUtil { public class DataSourceUtil {
@ -49,9 +51,10 @@ public class DataSourceUtil {
DataSourceUtil.rawDataSourceFactory = factory; DataSourceUtil.rawDataSourceFactory = factory;
} }
public static DataSource.Factory getDefaultDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter) {
if (defaultDataSourceFactory == null) { public static DataSource.Factory getDefaultDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter); if (defaultDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) {
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter, requestHeaders);
} }
return defaultDataSourceFactory; return defaultDataSourceFactory;
} }
@ -64,17 +67,21 @@ public class DataSourceUtil {
return new RawResourceDataSourceFactory(context.getApplicationContext()); 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, 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(); OkHttpClient client = OkHttpClientProvider.getOkHttpClient();
CookieJarContainer container = (CookieJarContainer) client.cookieJar(); CookieJarContainer container = (CookieJarContainer) client.cookieJar();
ForwardingCookieHandler handler = new ForwardingCookieHandler(context); ForwardingCookieHandler handler = new ForwardingCookieHandler(context);
container.setCookieJar(new JavaNetCookieJar(handler)); 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.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.lang.Math; import java.lang.Math;
import java.util.Map;
import java.lang.Object; import java.lang.Object;
import java.util.ArrayList; import java.util.ArrayList;
@ -103,7 +104,7 @@ class ReactExoplayerView extends FrameLayout implements
private boolean loadVideoStarted; private boolean loadVideoStarted;
private boolean isFullscreen; private boolean isFullscreen;
private boolean isInBackground; private boolean isInBackground;
private boolean isPaused = true; private boolean isPaused;
private boolean isBuffering; private boolean isBuffering;
private float rate = 1f; private float rate = 1f;
@ -118,6 +119,7 @@ class ReactExoplayerView extends FrameLayout implements
private float mProgressUpdateInterval = 250.0f; private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false; private boolean playInBackground = false;
private boolean useTextureView = false; private boolean useTextureView = false;
private Map<String, String> requestHeaders;
// \ End props // \ End props
// React // React
@ -135,7 +137,7 @@ class ReactExoplayerView extends FrameLayout implements
&& player.getPlayWhenReady() && player.getPlayWhenReady()
) { ) {
long pos = player.getCurrentPosition(); long pos = player.getCurrentPosition();
long bufferedDuration = player.getBufferedPercentage() * player.getDuration(); long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration()); eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration());
msg = obtainMessage(SHOW_PROGRESS); msg = obtainMessage(SHOW_PROGRESS);
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval)); sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
@ -417,7 +419,7 @@ class ReactExoplayerView extends FrameLayout implements
* @return A new DataSource factory. * @return A new DataSource factory.
*/ */
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) { 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 // AudioManager.OnAudioFocusChangeListener implementation
@ -661,14 +663,15 @@ class ReactExoplayerView extends FrameLayout implements
// ReactExoplayerViewManager public api // 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) { if (uri != null) {
boolean isOriginalSourceNull = srcUri == null; boolean isOriginalSourceNull = srcUri == null;
boolean isSourceEqual = uri.equals(srcUri); boolean isSourceEqual = uri.equals(srcUri);
this.srcUri = uri; this.srcUri = uri;
this.extension = extension; 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) { if (!isOriginalSourceNull && !isSourceEqual) {
reloadSource(); reloadSource();

View File

@ -13,6 +13,7 @@ import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.exoplayer2.upstream.RawResourceDataSource; import com.google.android.exoplayer2.upstream.RawResourceDataSource;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; 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 = "src";
private static final String PROP_SRC_URI = "uri"; private static final String PROP_SRC_URI = "uri";
private static final String PROP_SRC_TYPE = "type"; 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_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat"; private static final String PROP_REPEAT = "repeat";
private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack"; private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack";
@ -80,6 +82,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
Context context = videoView.getContext().getApplicationContext(); Context context = videoView.getContext().getApplicationContext();
String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null; 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; 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)) { if (TextUtils.isEmpty(uriString)) {
return; return;
@ -89,7 +93,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
Uri srcUri = Uri.parse(uriString); Uri srcUri = Uri.parse(uriString);
if (srcUri != null) { if (srcUri != null) {
videoView.setSrc(srcUri, extension); videoView.setSrc(srcUri, extension, headers);
} }
} else { } else {
int identifier = context.getResources().getIdentifier( int identifier = context.getResources().getIdentifier(
@ -208,4 +212,28 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
} }
return ResizeMode.RESIZE_MODE_FIT; 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.android.vending.expansion.zipfile.ZipResourceFile;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.uimanager.events.RCTEventEmitter;
@ -30,6 +31,8 @@ import java.util.Map;
import java.lang.Math; import java.lang.Math;
import java.math.BigDecimal; import java.math.BigDecimal;
import javax.annotation.Nullable;
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnPreparedListener, MediaPlayer public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnPreparedListener, MediaPlayer
.OnErrorListener, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener, LifecycleEventListener, MediaController.MediaPlayerControl { .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 mSrcUriString = null;
private String mSrcType = "mp4"; private String mSrcType = "mp4";
private ReadableMap mRequestHeaders = null;
private boolean mSrcIsNetwork = false; private boolean mSrcIsNetwork = false;
private boolean mSrcIsAsset = false; private boolean mSrcIsAsset = false;
private ScalableType mResizeMode = ScalableType.LEFT_TOP; 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 mRate = 1.0f;
private float mActiveRate = 1.0f; private float mActiveRate = 1.0f;
private boolean mPlayInBackground = false; private boolean mPlayInBackground = false;
private boolean mActiveStatePauseStatus = false; private boolean mBackgroundPaused = false;
private boolean mActiveStatePauseStatusInitialized = false;
private int mMainVer = 0; private int mMainVer = 0;
private int mPatchVer = 0; private int mPatchVer = 0;
@ -128,7 +131,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
@Override @Override
public void run() { public void run() {
if (mMediaPlayerValid && !isCompleted &&!mPaused) { if (mMediaPlayerValid && !isCompleted && !mPaused && !mBackgroundPaused) {
WritableMap event = Arguments.createMap(); WritableMap event = Arguments.createMap();
event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0); event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0);
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, mVideoBufferedDuration / 1000.0); //TODO:mBufferUpdateRunnable 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) { public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders) {
setSrc(uriString,type,isNetwork,isAsset,0,0); 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; mSrcUriString = uriString;
mSrcType = type; mSrcType = type;
mSrcIsNetwork = isNetwork; mSrcIsNetwork = isNetwork;
mSrcIsAsset = isAsset; mSrcIsAsset = isAsset;
mRequestHeaders = requestHeaders;
mMainVer = expansionMainVersion; mMainVer = expansionMainVersion;
mPatchVer = expansionPatchVersion; mPatchVer = expansionPatchVersion;
@ -245,7 +249,15 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
headers.put("Cookie", cookie); 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) { } else if (isAsset) {
if (uriString.startsWith("content://")) { if (uriString.startsWith("content://")) {
Uri parsedUrl = Uri.parse(uriString); Uri parsedUrl = Uri.parse(uriString);
@ -291,8 +303,13 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
} }
WritableMap src = Arguments.createMap(); WritableMap src = Arguments.createMap();
WritableMap wRequestHeaders = Arguments.createMap();
wRequestHeaders.merge(mRequestHeaders);
src.putString(ReactVideoViewManager.PROP_SRC_URI, uriString); src.putString(ReactVideoViewManager.PROP_SRC_URI, uriString);
src.putString(ReactVideoViewManager.PROP_SRC_TYPE, type); src.putString(ReactVideoViewManager.PROP_SRC_TYPE, type);
src.putMap(ReactVideoViewManager.PROP_SRC_HEADERS, wRequestHeaders);
src.putBoolean(ReactVideoViewManager.PROP_SRC_IS_NETWORK, isNetwork); src.putBoolean(ReactVideoViewManager.PROP_SRC_IS_NETWORK, isNetwork);
if(mMainVer>0) { if(mMainVer>0) {
src.putInt(ReactVideoViewManager.PROP_SRC_MAINVER, mMainVer); src.putInt(ReactVideoViewManager.PROP_SRC_MAINVER, mMainVer);
@ -334,11 +351,6 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
mPaused = paused; mPaused = paused;
if ( !mActiveStatePauseStatusInitialized ) {
mActiveStatePauseStatus = mPaused;
mActiveStatePauseStatusInitialized = true;
}
if (!mMediaPlayerValid) { if (!mMediaPlayerValid) {
return; return;
} }
@ -410,8 +422,16 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
if (mMediaPlayerValid) { if (mMediaPlayerValid) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!mPaused) { // Applying the rate while paused will cause the video to start if (!mPaused) { // Applying the rate while paused will cause the video to start
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate)); /* Per https://stackoverflow.com/questions/39442522/setplaybackparams-causes-illegalstateexception
mActiveRate = rate; * 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 { } else {
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android versions below 6.0"); 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(); super.onAttachedToWindow();
if(mMainVer>0) { if(mMainVer>0) {
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset,mMainVer,mPatchVer); setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders, mMainVer, mPatchVer);
} }
else { else {
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset); setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders);
} }
} }
@Override @Override
public void onHostPause() { public void onHostPause() {
if (mMediaPlayer != null && !mPlayInBackground) { if (mMediaPlayerValid && !mPaused && !mPlayInBackground) {
mActiveStatePauseStatus = mPaused; /* Pause the video in background
* Don't update the paused prop, developers should be able to update it on background
// Pause the video in background * so that when you return to the app the video is paused
setPausedModifier(true); */
mBackgroundPaused = true;
mMediaPlayer.pause();
} }
} }
@Override @Override
public void onHostResume() { public void onHostResume() {
if (mMediaPlayer != null && !mPlayInBackground) { mBackgroundPaused = false;
if (mMediaPlayerValid && !mPlayInBackground && !mPaused) {
new Handler().post(new Runnable() { new Handler().post(new Runnable() {
@Override @Override
public void run() { public void run() {
// Restore original state // Restore original state
setPausedModifier(mActiveStatePauseStatus); setPausedModifier(false);
} }
}); });
} }
} }
@Override @Override
public void onHostDestroy() { 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 = "src";
public static final String PROP_SRC_URI = "uri"; public static final String PROP_SRC_URI = "uri";
public static final String PROP_SRC_TYPE = "type"; 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_IS_NETWORK = "isNetwork";
public static final String PROP_SRC_MAINVER = "mainVer"; public static final String PROP_SRC_MAINVER = "mainVer";
public static final String PROP_SRC_PATCHVER = "patchVer"; public static final String PROP_SRC_PATCHVER = "patchVer";
@ -86,6 +87,7 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
src.getString(PROP_SRC_TYPE), src.getString(PROP_SRC_TYPE),
src.getBoolean(PROP_SRC_IS_NETWORK), src.getBoolean(PROP_SRC_IS_NETWORK),
src.getBoolean(PROP_SRC_IS_ASSET), src.getBoolean(PROP_SRC_IS_ASSET),
src.getMap(PROP_SRC_HEADERS),
mainVer, mainVer,
patchVer patchVer
); );
@ -95,8 +97,9 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
src.getString(PROP_SRC_URI), src.getString(PROP_SRC_URI),
src.getString(PROP_SRC_TYPE), src.getString(PROP_SRC_TYPE),
src.getBoolean(PROP_SRC_IS_NETWORK), 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 */ /* Required to publish events */
RCTEventDispatcher *_eventDispatcher; RCTEventDispatcher *_eventDispatcher;
BOOL _playbackRateObserverRegistered; BOOL _playbackRateObserverRegistered;
BOOL _videoLoadStarted;
bool _pendingSeek; bool _pendingSeek;
float _pendingSeekTime; float _pendingSeekTime;
@ -96,7 +97,7 @@ static NSString *const timedMetadata = @"timedMetadata";
- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem { - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem {
RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init]; RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init];
playerLayer.showsPlaybackControls = NO; playerLayer.showsPlaybackControls = YES;
playerLayer.rctDelegate = self; playerLayer.rctDelegate = self;
playerLayer.view.frame = self.bounds; playerLayer.view.frame = self.bounds;
playerLayer.player = player; playerLayer.player = player;
@ -324,6 +325,7 @@ static NSString *const timedMetadata = @"timedMetadata";
} }
}); });
}); });
_videoLoadStarted = YES;
} }
- (NSURL*) urlFilePath:(NSString*) filepath { - (NSURL*) urlFilePath:(NSString*) filepath {
@ -350,14 +352,24 @@ static NSString *const timedMetadata = @"timedMetadata";
NSString *uri = [source objectForKey:@"uri"]; NSString *uri = [source objectForKey:@"uri"];
NSString *subtitleUri = _selectedTextTrack[@"uri"]; NSString *subtitleUri = _selectedTextTrack[@"uri"];
NSString *type = [source objectForKey:@"type"]; NSString *type = [source objectForKey:@"type"];
NSDictionary *headers = [source objectForKey:@"requestHeaders"];
AVURLAsset *asset; AVURLAsset *asset;
AVURLAsset *subAsset; AVURLAsset *subAsset;
if (isNetwork) { 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]; NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:@{AVURLAssetHTTPCookiesKey : cookies}]; [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey];
subAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:subtitleUri] options:@{AVURLAssetHTTPCookiesKey : cookies}];
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 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 - (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 // When timeMetadata is read the event onTimedMetadata is triggered
if ([keyPath isEqualToString: timedMetadata]) if ([keyPath isEqualToString:timedMetadata]) {
{ NSArray<AVMetadataItem *> *items = [change objectForKey:@"new"];
if (items && ![items isEqual:[NSNull null]] && items.count > 0) {
NSMutableArray *array = [NSMutableArray new];
NSArray<AVMetadataItem *> *items = [change objectForKey:@"new"]; for (AVMetadataItem *item in items) {
if (items && ![items isEqual:[NSNull null]] && items.count > 0) { NSString *value = item.value;
NSString *identifier = item.identifier;
NSMutableArray *array = [NSMutableArray new];
for (AVMetadataItem *item in items) { if (![value isEqual: [NSNull null]]) {
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]];
NSString *value = item.value;
NSString *identifier = item.identifier; [array addObject:dictionary];
}
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
});
} }
self.onTimedMetadata(@{
@"target": self.reactTag,
@"metadata": array
});
}
} }
if ([keyPath isEqualToString:statusKeyPath]) { if ([keyPath isEqualToString:statusKeyPath]) {
// Handle player item status change. // Handle player item status change.
if (_playerItem.status == AVPlayerItemStatusReadyToPlay) { if (_playerItem.status == AVPlayerItemStatusReadyToPlay) {
float duration = CMTimeGetSeconds(_playerItem.asset.duration); float duration = CMTimeGetSeconds(_playerItem.asset.duration);
if (isnan(duration)) { if (isnan(duration)) {
duration = 0.0; duration = 0.0;
} }
NSObject *width = @"undefined"; NSObject *width = @"undefined";
NSObject *height = @"undefined"; NSObject *height = @"undefined";
NSString *orientation = @"undefined"; NSString *orientation = @"undefined";
if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) { 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 AVAssetTrack *videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
&& videoAsset.naturalSize.height == preferredTransform.ty) width = [NSNumber numberWithFloat:videoTrack.naturalSize.width];
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0)) 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"; orientation = @"landscape";
} else } else {
orientation = @"portrait"; orientation = @"portrait";
}
} }
if(self.onVideoLoad) { if (self.onVideoLoad && _videoLoadStarted) {
self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration], self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration],
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)], @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)],
@"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse], @"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse],
@ -473,21 +481,21 @@ static NSString *const timedMetadata = @"timedMetadata";
@"canStepBackward": [NSNumber numberWithBool:_playerItem.canStepBackward], @"canStepBackward": [NSNumber numberWithBool:_playerItem.canStepBackward],
@"canStepForward": [NSNumber numberWithBool:_playerItem.canStepForward], @"canStepForward": [NSNumber numberWithBool:_playerItem.canStepForward],
@"naturalSize": @{ @"naturalSize": @{
@"width": width, @"width": width,
@"height": height, @"height": height,
@"orientation": orientation @"orientation": orientation
}, },
@"textTracks": [self getTextTrackInfo], @"textTracks": [self getTextTrackInfo],
@"target": self.reactTag}); @"target": self.reactTag});
} }
_videoLoadStarted = NO;
[self attachListeners]; [self attachListeners];
[self applyModifiers]; [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], self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code],
@"domain": _playerItem.error.domain}, @"domain": _playerItem.error.domain},
@"target": self.reactTag}); @"target": self.reactTag});
} }
} else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) { } else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) {
_playerBufferEmpty = YES; _playerBufferEmpty = YES;
@ -500,28 +508,28 @@ static NSString *const timedMetadata = @"timedMetadata";
_playerBufferEmpty = NO; _playerBufferEmpty = NO;
self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag}); self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag});
} }
} else if (object == _playerLayer) { } else if (object == _playerLayer) {
if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) { if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) {
if([change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) { if([change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) {
self.onReadyForDisplay(@{@"target": self.reactTag}); self.onReadyForDisplay(@{@"target": self.reactTag});
} }
} }
} else if (object == _player) { } else if (object == _player) {
if([keyPath isEqualToString:playbackRate]) { if([keyPath isEqualToString:playbackRate]) {
if(self.onPlaybackRateChange) { if(self.onPlaybackRateChange) {
self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate], self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag}); @"target": self.reactTag});
}
if(_playbackStalled && _player.rate > 0) {
if(self.onPlaybackResume) {
self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
_playbackStalled = NO;
}
} }
if(_playbackStalled && _player.rate > 0) {
if(self.onPlaybackResume) {
self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
_playbackStalled = NO;
}
}
} else { } 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": [ "name": "react-native-video",
[ "version": "3.0.0",
"git://github.com/nfb-onf/react-native-video.git", "description": "A <Video /> element for react-native",
"/Users/amishra/Development/react_films" "main": "Video.js",
] "license": "MIT",
], "author": "Brent Vatne <brentvatne@gmail.com> (https://github.com/brentvatne)",
"_from": "git://github.com/nfb-onf/react-native-video.git", "contributors": [
"_id": "react-native-video@git://github.com/nfb-onf/react-native-video.git#8ce39e5b82108e6b9ea8549bd72ba58e95f04647", {
"_inBundle": false, "name": "Isaiah Grey",
"_integrity": "", "email": "isaiahgrey@gmail.com"
"_location": "/react-native-video", },
"_phantomChildren": {}, {
"_requested": { "name": "Johannes Lumpe",
"type": "git", "email": "johannes@lum.pe"
"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", "name": "Baris Sencan",
"fetchSpec": "git://github.com/nfb-onf/react-native-video.git", "email": "baris.sncn@gmail.com"
"gitCommittish": null },
}, {
"_requiredBy": [ "name": "Hampton Maxwell",
"/" "email": "me@hamptonmaxwell.com"
], }
"_resolved": "git://github.com/nfb-onf/react-native-video.git#8ce39e5b82108e6b9ea8549bd72ba58e95f04647", ],
"_spec": "git://github.com/nfb-onf/react-native-video.git", "repository": {
"_where": "/Users/amishra/Development/react_films", "type": "git",
"author": { "url": "git@github.com:brentvatne/react-native-video.git"
"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"
}, },
{ "devDependencies": {
"name": "Johannes Lumpe", "jest-cli": "0.2.1",
"email": "johannes@lum.pe" "eslint": "1.10.3",
"babel-eslint": "5.0.0-beta8",
"eslint-plugin-react": "3.16.1",
"eslint-config-airbnb": "4.0.0"
}, },
{ "dependencies": {
"name": "Baris Sencan", "keymirror": "0.1.1",
"email": "baris.sncn@gmail.com" "prop-types": "^15.5.10"
}, },
{ "scripts": {
"name": "Hampton Maxwell", "test": "node_modules/.bin/eslint *.js"
"email": "me@hamptonmaxwell.com" },
"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"
} }