iOS Sidecar loading for captions and offline support (isAsset). Android

fix to respect User settings for captions.
This commit is contained in:
Ash Mishra 2018-06-21 09:08:37 -07:00
parent d3d1947beb
commit 3e2e16ef44
4 changed files with 294 additions and 125 deletions

View File

@ -11,6 +11,7 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.Window; import android.view.Window;
import android.view.accessibility.CaptioningManager;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import com.brentvatne.react.R; import com.brentvatne.react.R;
@ -759,7 +760,14 @@ class ReactExoplayerView extends FrameLayout implements
trackIndex = value.asInt(); trackIndex = value.asInt();
} else { // default. invalid type or "system" } else { // default. invalid type or "system"
trackSelector.clearSelectionOverrides(index); trackSelector.clearSelectionOverrides(index);
return; trackSelector.setSelectionOverride(index, groups, null);
int sdk = android.os.Build.VERSION.SDK_INT;
if (sdk>18 && groups.length>0) {
CaptioningManager captioningManager = (CaptioningManager) themedReactContext.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager.isEnabled()) trackIndex=0;
} else return;
} }
if (trackIndex == C.INDEX_UNSET) { if (trackIndex == C.INDEX_UNSET) {

View File

@ -3,6 +3,8 @@
#import <React/RCTBridgeModule.h> #import <React/RCTBridgeModule.h>
#import <React/RCTEventDispatcher.h> #import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h> #import <React/UIView+React.h>
#include <MediaAccessibility/MediaAccessibility.h>
#include <AVFoundation/AVFoundation.h>
static NSString *const statusKeyPath = @"status"; static NSString *const statusKeyPath = @"status";
static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp"; static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp";
@ -18,7 +20,6 @@ static NSString *const timedMetadata = @"timedMetadata";
BOOL _playerItemObserversSet; BOOL _playerItemObserversSet;
BOOL _playerBufferEmpty; BOOL _playerBufferEmpty;
AVPlayerLayer *_playerLayer; AVPlayerLayer *_playerLayer;
BOOL _playerLayerObserverSet;
AVPlayerViewController *_playerViewController; AVPlayerViewController *_playerViewController;
NSURL *_videoURL; NSURL *_videoURL;
@ -42,6 +43,7 @@ static NSString *const timedMetadata = @"timedMetadata";
BOOL _paused; BOOL _paused;
BOOL _repeat; BOOL _repeat;
BOOL _allowsExternalPlayback; BOOL _allowsExternalPlayback;
NSArray * _textTracks;
NSDictionary * _selectedTextTrack; NSDictionary * _selectedTextTrack;
BOOL _playbackStalled; BOOL _playbackStalled;
BOOL _playInBackground; BOOL _playInBackground;
@ -283,6 +285,10 @@ static NSString *const timedMetadata = @"timedMetadata";
[self removePlayerLayer]; [self removePlayerLayer];
[self removePlayerTimeObserver]; [self removePlayerTimeObserver];
[self removePlayerItemObservers]; [self removePlayerItemObservers];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// perform on next run loop, otherwise other passed react-props may not be set
_playerItem = [self playerItemForSource:source]; _playerItem = [self playerItemForSource:source];
[self addPlayerItemObservers]; [self addPlayerItemObservers];
@ -304,6 +310,7 @@ static NSString *const timedMetadata = @"timedMetadata";
[self addPlayerTimeObserver]; [self addPlayerTimeObserver];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//Perform on next run loop, otherwise onVideoLoadStart is nil //Perform on next run loop, otherwise onVideoLoadStart is nil
if(self.onVideoLoadStart) { if(self.onVideoLoadStart) {
id uri = [source objectForKey:@"uri"]; id uri = [source objectForKey:@"uri"];
@ -316,6 +323,24 @@ static NSString *const timedMetadata = @"timedMetadata";
}); });
} }
}); });
});
}
- (NSURL*) urlFilePath:(NSString*) filepath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* relativeFilePath = [filepath lastPathComponent];
// the file may be multiple levels below the documents directory
NSArray* fileComponents = [filepath componentsSeparatedByString:@"Documents/"];
if (fileComponents.count>1) {
relativeFilePath = [fileComponents objectAtIndex:1];
}
NSString *path = [paths.firstObject stringByAppendingPathComponent:relativeFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
return [NSURL fileURLWithPath:path];
}
return nil;
} }
- (AVPlayerItem*)playerItemForSource:(NSDictionary *)source - (AVPlayerItem*)playerItemForSource:(NSDictionary *)source
@ -323,23 +348,58 @@ static NSString *const timedMetadata = @"timedMetadata";
bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]]; bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]];
bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]]; bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]];
NSString *uri = [source objectForKey:@"uri"]; NSString *uri = [source objectForKey:@"uri"];
NSString *subtitleUri = _selectedTextTrack[@"uri"];
NSString *type = [source objectForKey:@"type"]; NSString *type = [source objectForKey:@"type"];
NSURL *url = (isNetwork || isAsset) ? AVURLAsset *asset;
[NSURL URLWithString:uri] : AVURLAsset *subAsset;
[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];
if (isNetwork) { if (isNetwork) {
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:@{AVURLAssetHTTPCookiesKey : cookies}]; asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:@{AVURLAssetHTTPCookiesKey : cookies}];
return [AVPlayerItem playerItemWithAsset:asset]; subAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:subtitleUri] options:@{AVURLAssetHTTPCookiesKey : cookies}];
} }
else if (isAsset) { else if (isAsset) // assets on iOS have to be in the Documents folder
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil]; {
return [AVPlayerItem playerItemWithAsset:asset]; asset = [AVURLAsset URLAssetWithURL:[self urlFilePath:uri] options:nil];
subAsset = [AVURLAsset URLAssetWithURL:[self urlFilePath:subtitleUri] options:nil];
}
else if (!isNetwork && !isAsset) // this is a relative file hardcoded in the app?
{
asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil];
} }
return [AVPlayerItem playerItemWithURL:url]; if (!_textTracks || !_selectedTextTrack) return [AVPlayerItem playerItemWithAsset:asset];
// otherwise sideload text tracks
AVAssetTrack *videoAsset = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;
AVAssetTrack *audioAsset = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject;
AVAssetTrack *subtitleAsset = [subAsset tracksWithMediaType:AVMediaTypeText].firstObject;
NSLog(@"assets: %@,%@,%@", videoAsset, audioAsset, subtitleAsset);
AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
AVMutableCompositionTrack *videoCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *audioCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *subtitleCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeText preferredTrackID:kCMPersistentTrackID_Invalid];
[videoCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration)
ofTrack:videoAsset
atTime:kCMTimeZero
error:nil];
[audioCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration)
ofTrack:audioAsset
atTime:kCMTimeZero
error:nil];
[subtitleCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration)
ofTrack:subtitleAsset
atTime:kCMTimeZero
error:nil];
return [AVPlayerItem playerItemWithAsset:mixComposition];
} }
- (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
@ -388,13 +448,14 @@ static NSString *const timedMetadata = @"timedMetadata";
NSString *orientation = @"undefined"; NSString *orientation = @"undefined";
if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) { if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) {
AVAssetTrack *videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; AVAssetTrack *videoAsset = [[_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 width = [NSNumber numberWithFloat:videoAsset.naturalSize.width];
&& videoTrack.naturalSize.height == preferredTransform.ty) 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)) || (preferredTransform.tx == 0 && preferredTransform.ty == 0))
{ {
orientation = @"landscape"; orientation = @"landscape";
@ -569,28 +630,21 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)setCurrentTime:(float)currentTime - (void)setCurrentTime:(float)currentTime
{ {
NSDictionary *info = @{ [self setSeek: currentTime];
@"time": [NSNumber numberWithFloat:currentTime],
@"tolerance": [NSNumber numberWithInt:100]
};
[self setSeek:info];
} }
- (void)setSeek:(NSDictionary *)info - (void)setSeek:(float)seekTime
{ {
NSNumber *seekTime = info[@"time"]; int timeScale = 10000;
NSNumber *seekTolerance = info[@"tolerance"];
int timeScale = 1000;
AVPlayerItem *item = _player.currentItem; AVPlayerItem *item = _player.currentItem;
if (item && item.status == AVPlayerItemStatusReadyToPlay) { if (item && item.status == AVPlayerItemStatusReadyToPlay) {
// TODO check loadedTimeRanges // TODO check loadedTimeRanges
CMTime cmSeekTime = CMTimeMakeWithSeconds([seekTime floatValue], timeScale); CMTime cmSeekTime = CMTimeMakeWithSeconds(seekTime, timeScale);
CMTime current = item.currentTime; CMTime current = item.currentTime;
// TODO figure out a good tolerance level // TODO figure out a good tolerance level
CMTime tolerance = CMTimeMake([seekTolerance floatValue], timeScale); CMTime tolerance = CMTimeMake(1000, timeScale);
BOOL wasPaused = _paused; BOOL wasPaused = _paused;
if (CMTimeCompare(current, cmSeekTime) != 0) { if (CMTimeCompare(current, cmSeekTime) != 0) {
@ -604,7 +658,7 @@ static NSString *const timedMetadata = @"timedMetadata";
} }
if(self.onVideoSeek) { if(self.onVideoSeek) {
self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)], self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)],
@"seekTime": seekTime, @"seekTime": [NSNumber numberWithFloat:seekTime],
@"target": self.reactTag}); @"target": self.reactTag});
} }
}]; }];
@ -615,7 +669,7 @@ static NSString *const timedMetadata = @"timedMetadata";
} else { } else {
// TODO: See if this makes sense and if so, actually implement it // TODO: See if this makes sense and if so, actually implement it
_pendingSeek = true; _pendingSeek = true;
_pendingSeekTime = [seekTime floatValue]; _pendingSeekTime = seekTime;
} }
} }
@ -660,8 +714,77 @@ static NSString *const timedMetadata = @"timedMetadata";
} }
- (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack {
_selectedTextTrack = selectedTextTrack; _selectedTextTrack = selectedTextTrack;
NSString *type = selectedTextTrack[@"type"];
// when textTracks exist, we set side-loaded subtitles
if (_textTracks) {
[self setSideloadedTextTrack];
return;
}
// otherwise check for subtitles in the streaming asset (.m3u8)
[self setStreamingTextTrack];
}
- (void)setSideloadedTextTrack {
NSString *type = _selectedTextTrack[@"type"];
NSArray* textTracks = [self getTextTrackInfo];
if (textTracks==nil) {
_selectedTextTrack = nil;
return;
}
NSDictionary* selectedCaption = nil;
if ([type isEqualToString:@"disabled"]) {
// Do nothing. We want to ensure option is nil
} else if ([type isEqualToString:@"language"]) {
NSString *selectedValue = _selectedTextTrack[@"value"];
for (int i = 0; i < textTracks.count; ++i) {
NSDictionary *currentCaption = [textTracks objectAtIndex:i];
if ([selectedValue isEqualToString:currentCaption[@"language"]]) {
selectedCaption = currentCaption;
break;
}
}
} else if ([type isEqualToString:@"title"]) {
NSString *selectedValue = _selectedTextTrack[@"value"];
for (int i = 0; i < textTracks.count; ++i) {
NSDictionary *currentCaption = [textTracks objectAtIndex:i];
if ([selectedValue isEqualToString:currentCaption[@"title"]]) {
selectedCaption = currentCaption;
break;
}
}
} else if ([type isEqualToString:@"index"]) {
if ([_selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) {
int index = [_selectedTextTrack[@"value"] intValue];
if (textTracks.count > index) {
selectedCaption = [textTracks objectAtIndex:index];
}
}
}
// user's selected language might not be available, or system defaults have captions enabled
if (selectedCaption==nil || [type isEqualToString:@"default"]) {
CFArrayRef captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser);
NSArray *captionSettings = (__bridge NSArray*)captioningMediaCharacteristics;
if ([captionSettings containsObject: AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]) {
selectedCaption = textTracks.firstObject;
}
}
_selectedTextTrack = selectedCaption;
}
-(void) setStreamingTextTrack {
NSString *type = _selectedTextTrack[@"type"];
AVMediaSelectionGroup *group = [_player.currentItem.asset AVMediaSelectionGroup *group = [_player.currentItem.asset
mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
AVMediaSelectionOption *option; AVMediaSelectionOption *option;
@ -669,7 +792,7 @@ static NSString *const timedMetadata = @"timedMetadata";
if ([type isEqualToString:@"disabled"]) { if ([type isEqualToString:@"disabled"]) {
// Do nothing. We want to ensure option is nil // Do nothing. We want to ensure option is nil
} else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) {
NSString *value = selectedTextTrack[@"value"]; NSString *value = _selectedTextTrack[@"value"];
for (int i = 0; i < group.options.count; ++i) { for (int i = 0; i < group.options.count; ++i) {
AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i];
NSString *optionValue; NSString *optionValue;
@ -688,8 +811,8 @@ static NSString *const timedMetadata = @"timedMetadata";
//} else if ([type isEqualToString:@"default"]) { //} else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */ // option = group.defaultOption; */
} else if ([type isEqualToString:@"index"]) { } else if ([type isEqualToString:@"index"]) {
if ([selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) { if ([_selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) {
int index = [selectedTextTrack[@"value"] intValue]; int index = [_selectedTextTrack[@"value"] intValue];
if (group.options.count > index) { if (group.options.count > index) {
option = [group.options objectAtIndex:index]; option = [group.options objectAtIndex:index];
} }
@ -703,23 +826,30 @@ static NSString *const timedMetadata = @"timedMetadata";
[_player.currentItem selectMediaOption:option inMediaSelectionGroup:group]; [_player.currentItem selectMediaOption:option inMediaSelectionGroup:group];
} }
- (void)setTextTracks:(NSArray*) textTracks;
{
_textTracks = textTracks;
}
- (NSArray *)getTextTrackInfo - (NSArray *)getTextTrackInfo
{ {
// if sideloaded, textTracks will already be set
if (_textTracks) return _textTracks;
// if streaming video, we extract the text tracks
NSMutableArray *textTracks = [[NSMutableArray alloc] init]; NSMutableArray *textTracks = [[NSMutableArray alloc] init];
AVMediaSelectionGroup *group = [_player.currentItem.asset AVMediaSelectionGroup *group = [_player.currentItem.asset
mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
for (int i = 0; i < group.options.count; ++i) { for (int i = 0; i < group.options.count; ++i) {
AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i];
NSString *title = @""; NSString *title = [[[currentOption commonMetadata]
NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; valueForKey:@"value"]
if (values.count > 0) { objectAtIndex:0];
title = [values objectAtIndex:0];
}
NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @"";
NSDictionary *textTrack = @{ NSDictionary *textTrack = @{
@"index": [NSNumber numberWithInt:i], @"index": [NSNumber numberWithInt:i],
@"title": title, @"title": title,
@"language": language @"language": [currentOption extendedLanguageTag]
}; };
[textTracks addObject:textTrack]; [textTracks addObject:textTrack];
} }
@ -802,7 +932,6 @@ static NSString *const timedMetadata = @"timedMetadata";
// resize mode must be set before layer is added // resize mode must be set before layer is added
[self setResizeMode:_resizeMode]; [self setResizeMode:_resizeMode];
[_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; [_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil];
_playerLayerObserverSet = YES;
[self.layer addSublayer:_playerLayer]; [self.layer addSublayer:_playerLayer];
self.layer.needsDisplayOnBoundsChange = YES; self.layer.needsDisplayOnBoundsChange = YES;
@ -841,10 +970,7 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)removePlayerLayer - (void)removePlayerLayer
{ {
[_playerLayer removeFromSuperlayer]; [_playerLayer removeFromSuperlayer];
if (_playerLayerObserverSet) {
[_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath];
_playerLayerObserverSet = NO;
}
_playerLayer = nil; _playerLayer = nil;
} }

View File

@ -23,6 +23,7 @@ RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL); RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL); RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL);
RCT_EXPORT_VIEW_PROPERTY(textTracks, NSArray);
RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL); RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
RCT_EXPORT_VIEW_PROPERTY(muted, BOOL); RCT_EXPORT_VIEW_PROPERTY(muted, BOOL);

View File

@ -1,10 +1,38 @@
{ {
"name": "react-native-video", "_args": [
"version": "2.2.0", [
"description": "A <Video /> element for react-native", "git://github.com/nfb-onf/react-native-video.git",
"main": "Video.js", "/Users/amishra/Development/react_films"
"license": "MIT", ]
"author": "Brent Vatne <brentvatne@gmail.com> (https://github.com/brentvatne)", ],
"_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": [ "contributors": [
{ {
"name": "Isaiah Grey", "name": "Isaiah Grey",
@ -23,27 +51,33 @@
"email": "me@hamptonmaxwell.com" "email": "me@hamptonmaxwell.com"
} }
], ],
"repository": {
"type": "git",
"url": "git@github.com:brentvatne/react-native-video.git"
},
"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"
},
"dependencies": { "dependencies": {
"keymirror": "0.1.1", "keymirror": "0.1.1",
"prop-types": "^15.5.10" "prop-types": "^15.5.10"
}, },
"scripts": { "description": "A <Video /> element for react-native",
"test": "node_modules/.bin/eslint *.js" "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": { "rnpm": {
"android": { "android": {
"sourceDir": "./android-exoplayer" "sourceDir": "./android-exoplayer"
} }
} },
"scripts": {
"test": "eslint *.js"
},
"version": "2.2.0"
} }