Merge pull request #1306 from nickgzzjr/master
Video Filters and Save Video
This commit is contained in:
commit
5e684d40c8
@ -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
18
FilterType.js
Normal 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'
|
||||
};
|
59
README.md
59
README.md
@ -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:
|
||||
|
27
Video.js
27
Video.js
@ -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([
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -1,5 +1,6 @@
|
||||
#import <React/RCTViewManager.h>
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCTVideoManager : RCTViewManager
|
||||
@interface RCTVideoManager : RCTViewManager <RCTBridgeModule>
|
||||
|
||||
@end
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user