Merge remote-tracking branch 'upstream/master'
# Conflicts: # ios/RCTVideo.m # package.json
This commit is contained in:
commit
05d4be2d9c
11
CHANGELOG.md
11
CHANGELOG.md
@ -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
117
README.md
@ -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` don’t works properly with the tvOS target so we need to add the library manually.
|
||||
`react-native link` doesn’t 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)
|
||||
|
24
Video.js
24
Video.js
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
150
ios/RCTVideo.m
150
ios/RCTVideo.m
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
120
package.json
120
package.json
@ -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"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user