Merge pull request #1306 from nickgzzjr/master

Video Filters and Save Video
This commit is contained in:
Hampton Maxwell 2018-11-20 23:47:59 -08:00 committed by GitHub
commit 5e684d40c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 278 additions and 26 deletions

View File

@ -17,6 +17,7 @@
* Switch useTextureView to default to `true` [#1286](https://github.com/react-native-community/react-native-video/pull/1286)
* Re-add fullscreenAutorotate prop [#1303](https://github.com/react-native-community/react-native-video/pull/1303)
* Make seek throw a useful error for NaN values [#1283](https://github.com/react-native-community/react-native-video/pull/1283)
* Video Filters and Save Video [#1306](https://github.com/react-native-community/react-native-video/pull/1306)
* Fix: volume should not change on onAudioFocusChange event [#1327](https://github.com/react-native-community/react-native-video/pull/1327)
### Version 3.2.0

18
FilterType.js Normal file
View File

@ -0,0 +1,18 @@
export default {
NONE: '',
INVERT: 'CIColorInvert',
MONOCHROME: 'CIColorMonochrome',
POSTERIZE: 'CIColorPosterize',
FALSE: 'CIFalseColor',
MAXIMUMCOMPONENT: 'CIMaximumComponent',
MINIMUMCOMPONENT: 'CIMinimumComponent',
CHROME: 'CIPhotoEffectChrome',
FADE: 'CIPhotoEffectFade',
INSTANT: 'CIPhotoEffectInstant',
MONO: 'CIPhotoEffectMono',
NOIR: 'CIPhotoEffectNoir',
PROCESS: 'CIPhotoEffectProcess',
TONAL: 'CIPhotoEffectTonal',
TRANSFER: 'CIPhotoEffectTransfer',
SEPIA: 'CISepiaTone'
};

View File

@ -259,6 +259,7 @@ var styles = StyleSheet.create({
* [audioOnly](#audioonly)
* [bufferConfig](#bufferconfig)
* [controls](#controls)
* [filter](#filter)
* [fullscreen](#fullscreen)
* [fullscreenAutorotate](#fullscreenautorotate)
* [fullscreenOrientation](#fullscreenorientation)
@ -299,6 +300,7 @@ var styles = StyleSheet.create({
### Methods
* [dismissFullscreenPlayer](#dismissfullscreenplayer)
* [presentFullscreenPlayer](#presentfullscreenplayer)
* [save](#save)
* [seek](#seek)
### Configurable props
@ -352,6 +354,33 @@ Note on iOS, controls are always shown when in fullscreen mode.
Platforms: iOS, react-native-dom
#### filter
Add video filter
* **FilterType.NONE (default)** - No Filter
* **FilterType.INVERT** - CIColorInvert
* **FilterType.MONOCHROME** - CIColorMonochrome
* **FilterType.POSTERIZE** - CIColorPosterize
* **FilterType.FALSE** - CIFalseColor
* **FilterType.MAXIMUMCOMPONENT** - CIMaximumComponent
* **FilterType.MINIMUMCOMPONENT** - CIMinimumComponent
* **FilterType.CHROME** - CIPhotoEffectChrome
* **FilterType.FADE** - CIPhotoEffectFade
* **FilterType.INSTANT** - CIPhotoEffectInstant
* **FilterType.MONO** - CIPhotoEffectMono
* **FilterType.NOIR** - CIPhotoEffectNoir
* **FilterType.PROCESS** - CIPhotoEffectProcess
* **FilterType.TONAL** - CIPhotoEffectTonal
* **FilterType.TRANSFER** - CIPhotoEffectTransfer
* **FilterType.SEPIA** - CISepiaTone
For more details on these filters refer to the [iOS docs](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW55).
Notes:
1. Using a filter can impact CPU usage. A workaround is to save the video with the filter and then load the saved video.
2. Video filter is currently not supported on HLS playlists.
Platforms: iOS
#### fullscreen
Controls whether the player enters fullscreen on play.
* **false (default)** - Don't display the video in fullscreen
@ -671,6 +700,7 @@ Adjust the volume.
Platforms: all
### Event props
#### onAudioBecomingNoisy
@ -879,6 +909,33 @@ this.player.presentFullscreenPlayer();
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
#### save
`save(): Promise`
Save video to your Photos with current filter prop. Returns promise.
Example:
```
let response = await this.save();
let path = response.uri;
```
Notes:
- Currently only supports highest quality export
- Currently only supports MP4 export
- Currently only supports exporting to user's cache directory with a generated UUID filename.
- User will need to remove the saved video through their Photos app
- Works with cached videos as well. (Checkout video-caching example)
- If the video is has not began buffering (e.g. there is no internet connection) then the save function will throw an error.
- If the video is buffering then the save function promise will return after the video has finished buffering and processing.
Future:
- Will support multiple qualities through options
- Will support more formats in the future through options
- Will support custom directory and file name through options
Platforms: iOS
#### seek()
`seek(seconds)`
@ -909,6 +966,8 @@ this.player.seek(120, 50); // Seek to 2 minutes with +/- 50 milliseconds accurac
Platforms: iOS
### iOS App Transport Security
- By default, iOS will only load encrypted (https) urls. If you want to load content from an unencrypted (http) source, you will need to modify your Info.plist file and add the following entry:

View File

@ -1,8 +1,9 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform} from 'react-native';
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle} from 'react-native';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
import TextTrackType from './TextTrackType';
import FilterType from './FilterType';
import VideoResizeMode from './VideoResizeMode.js';
const styles = StyleSheet.create({
@ -11,7 +12,7 @@ const styles = StyleSheet.create({
},
});
export { TextTrackType };
export { TextTrackType, FilterType };
export default class Video extends Component {
@ -73,6 +74,10 @@ export default class Video extends Component {
this.setNativeProps({ fullscreen: false });
};
save = async (options?) => {
return await NativeModules.VideoManager.save(options, findNodeHandle(this._root));
}
_assignRoot = (component) => {
this._root = component;
};
@ -277,6 +282,24 @@ export default class Video extends Component {
}
Video.propTypes = {
filter: PropTypes.oneOf([
FilterType.NONE,
FilterType.INVERT,
FilterType.MONOCHROME,
FilterType.POSTERIZE,
FilterType.FALSE,
FilterType.MAXIMUMCOMPONENT,
FilterType.MINIMUMCOMPONENT,
FilterType.CHROME,
FilterType.FADE,
FilterType.INSTANT,
FilterType.MONO,
FilterType.NOIR,
FilterType.PROCESS,
FilterType.TONAL,
FilterType.TRANSFER,
FilterType.SEPIA
]),
/* Native only */
src: PropTypes.object,
seek: PropTypes.oneOfType([

View File

@ -5,7 +5,7 @@
*/
import React, { Component } from "react";
import { StyleSheet, Text, View, Dimensions } from "react-native";
import { StyleSheet, Text, View, Dimensions, TouchableOpacity } from "react-native";
import Video from "react-native-video";
const { height, width } = Dimensions.get("screen");
@ -28,6 +28,16 @@ export default class App extends Component<Props> {
}}
style={{ flex: 1, height, width }}
/>
<TouchableOpacity
onPress={async () => {
let response = await this.player.save();
let uri = response.uri;
console.log("Download URI", uri);
}}
style={styles.button}
>
<Text style={{color: 'white'}}>Save</Text>
</TouchableOpacity>
</View>
);
}
@ -40,6 +50,14 @@ const styles = StyleSheet.create({
alignItems: "center",
backgroundColor: "#F5FCFF"
},
button: {
position: 'absolute',
top: 50,
right: 16,
padding: 10,
backgroundColor: '#9B2FAE',
borderRadius: 8
},
welcome: {
fontSize: 20,
textAlign: "center",

View File

@ -9,9 +9,9 @@ PODS:
- glog (0.3.4)
- React (0.56.0):
- React/Core (= 0.56.0)
- react-native-video/Video (3.1.0):
- react-native-video/Video (3.2.2):
- React
- react-native-video/VideoCaching (3.1.0):
- react-native-video/VideoCaching (3.2.2):
- DVAssetLoaderDelegate (~> 0.3.1)
- React
- react-native-video/Video

View File

@ -24,6 +24,21 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
@ -38,19 +53,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSAppTransportSecurity</key>
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>

View File

@ -4,6 +4,7 @@
#import "RCTVideoPlayerViewController.h"
#import "RCTVideoPlayerViewControllerDelegate.h"
#import <React/RCTComponent.h>
#import <React/RCTBridgeModule.h>
#if __has_include(<react-native-video/RCTVideoCache.h>)
#import <react-native-video/RCTVideoCache.h>
@ -41,4 +42,6 @@
- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem;
- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
@end

View File

@ -67,6 +67,7 @@ static int const RCTVideoUnset = -1;
BOOL _fullscreenAutorotate;
NSString * _fullscreenOrientation;
BOOL _fullscreenPlayerPresented;
NSString *_filterName;
UIViewController * _presentingViewController;
#if __has_include(<react-native-video/RCTVideoCache.h>)
RCTVideoCache * _videoCache;
@ -335,6 +336,7 @@ static int const RCTVideoUnset = -1;
[self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) {
_playerItem = playerItem;
[self addPlayerItemObservers];
[self setFilter:_filterName];
[_player pause];
[_playerViewController.view removeFromSuperview];
@ -1262,6 +1264,42 @@ static int const RCTVideoUnset = -1;
}
}
- (void)setFilter:(NSString *)filterName {
_filterName = filterName;
AVAsset *asset = _playerItem.asset;
if (asset != nil) {
CIFilter *filter = [CIFilter filterWithName:filterName];
_playerItem.videoComposition = [AVVideoComposition
videoCompositionWithAsset:asset
applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) {
if (filter == nil) {
[request finishWithImage:request.sourceImage context:nil];
} else {
CIImage *image = request.sourceImage.imageByClampingToExtent;
[filter setValue:image forKey:kCIInputImageKey];
CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent];
[request finishWithImage:output context:nil];
}
}];
}
}
#pragma mark - React View Management
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
@ -1348,4 +1386,78 @@ static int const RCTVideoUnset = -1;
[super removeFromSuperview];
}
#pragma mark - Export
- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
AVAsset *asset = _playerItem.asset;
if (asset != nil) {
AVAssetExportSession *exportSession = [AVAssetExportSession
exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality];
if (exportSession != nil) {
NSString *path = nil;
NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"]
withExtension:@".mp4"];
NSURL *url = [NSURL fileURLWithPath:path];
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.outputURL = url;
exportSession.videoComposition = _playerItem.videoComposition;
exportSession.shouldOptimizeForNetworkUse = true;
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusFailed:
reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error);
break;
case AVAssetExportSessionStatusCancelled:
reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error);
break;
default:
resolve(@{@"uri": url.absoluteString});
break;
}
}];
} else {
reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil);
}
} else {
reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil);
}
}
- (BOOL)ensureDirExistsWithPath:(NSString *)path {
BOOL isDir = NO;
NSError *error;
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
if (!(exists && isDir)) {
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
return NO;
}
}
return YES;
}
- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension {
NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
[self ensureDirExistsWithPath:directory];
return [directory stringByAppendingPathComponent:fileName];
}
- (NSString *)cacheDirectoryPath {
NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return array[0];
}
@end

View File

@ -1,5 +1,6 @@
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
@interface RCTVideoManager : RCTViewManager
@interface RCTVideoManager : RCTViewManager <RCTBridgeModule>
@end

View File

@ -1,14 +1,13 @@
#import "RCTVideoManager.h"
#import "RCTVideo.h"
#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>
#import <AVFoundation/AVFoundation.h>
@implementation RCTVideoManager
RCT_EXPORT_MODULE();
@synthesize bridge = _bridge;
- (UIView *)view
{
return [[RCTVideo alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
@ -16,7 +15,7 @@ RCT_EXPORT_MODULE();
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
return self.bridge.uiManager.methodQueue;
}
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
@ -39,6 +38,7 @@ RCT_EXPORT_VIEW_PROPERTY(currentTime, float);
RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullscreenAutorotate, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString);
RCT_EXPORT_VIEW_PROPERTY(filter, NSString);
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock);
@ -59,6 +59,22 @@ RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoSaved, RCTBubblingEventBlock);
RCT_REMAP_METHOD(save,
options:(NSDictionary *)options
reactTag:(nonnull NSNumber *)reactTag
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
[self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTVideo *> *viewRegistry) {
RCTVideo *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTVideo class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view);
} else {
[view save:options resolve:resolve reject:reject];
}
}];
}
- (NSDictionary *)constantsToExport
{

View File

@ -1,6 +1,6 @@
{
"name": "react-native-video",
"version": "3.2.1",
"version": "3.2.2",
"description": "A <Video /> element for react-native",
"main": "Video.js",
"license": "MIT",