Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video Filters and Save Video #1306

Merged
merged 15 commits into from
Nov 21, 2018
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Add fullscreenOrientation option for iOS [#1215](https://github.com/react-native-community/react-native-video/pull/1215)
* Update to ExoPlayer 2.9.0 [#1285](https://github.com/react-native-community/react-native-video/pull/1285)
* Switch useTextureView to default to `true` [#1286](https://github.com/react-native-community/react-native-video/pull/1286)
* Video Filters and Save Video [#1306](https://github.com/react-native-community/react-native-video/pull/1306)

### Version 3.2.0
* Basic fullscreen support for Android MediaPlayer [#1138](https://github.com/react-native-community/react-native-video/pull/1138)
Expand Down
18 changes: 18 additions & 0 deletions FilterType.js
Original file line number Diff line number Diff line change
@@ -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'
};
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ var styles = StyleSheet.create({
* [audioOnly](#audioonly)
* [bufferConfig](#bufferconfig)
* [controls](#controls)
* [filter](#filter)
* [fullscreen](#fullscreen)
* [fullscreenOrientation](#fullscreenorientation)
* [headers](#headers)
Expand Down Expand Up @@ -298,6 +299,7 @@ var styles = StyleSheet.create({
### Methods
* [dismissFullscreenPlayer](#dismissfullscreenplayer)
* [presentFullscreenPlayer](#presentfullscreenplayer)
* [save](#save)
* [seek](#seek)

### Configurable props
Expand Down Expand Up @@ -351,6 +353,29 @@ 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).

Platforms: iOS

#### fullscreen
Controls whether the player enters fullscreen on play.
* **false (default)** - Don't display the video in fullscreen
Expand Down Expand Up @@ -665,6 +690,7 @@ Adjust the volume.

Platforms: all


### Event props

#### onAudioBecomingNoisy
Expand Down Expand Up @@ -873,6 +899,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)`

Expand Down Expand Up @@ -903,6 +956,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:
Expand Down
27 changes: 25 additions & 2 deletions Video.js
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -11,7 +12,7 @@ const styles = StyleSheet.create({
},
});

export { TextTrackType };
export { TextTrackType, FilterType };

export default class Video extends Component {

Expand Down Expand Up @@ -71,6 +72,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;
};
Expand Down Expand Up @@ -275,6 +280,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([
Expand Down
20 changes: 19 additions & 1 deletion examples/video-caching/App.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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>
);
}
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions ios/Video/RCTVideo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -41,4 +42,6 @@

- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem;

- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;

@end
114 changes: 113 additions & 1 deletion ios/Video/RCTVideo.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ @implementation RCTVideo
BOOL _fullscreen;
NSString * _fullscreenOrientation;
BOOL _fullscreenPlayerPresented;
NSString *_filterName;
UIViewController * _presentingViewController;
#if __has_include(<react-native-video/RCTVideoCache.h>)
RCTVideoCache * _videoCache;
Expand Down Expand Up @@ -861,6 +862,7 @@ - (void)applyModifiers
[self setResizeMode:_resizeMode];
[self setRepeat:_repeat];
[self setPaused:_paused];
[self setFilter:_filterName];
[self setControls:_controls];
[self setAllowsExternalPlayback:_allowsExternalPlayback];
}
Expand Down Expand Up @@ -1252,6 +1254,42 @@ - (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerView
}
}

- (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
Expand Down Expand Up @@ -1338,4 +1376,78 @@ - (void)removeFromSuperview
[super removeFromSuperview];
}

@end
#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
3 changes: 2 additions & 1 deletion ios/Video/RCTVideoManager.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>

@interface RCTVideoManager : RCTViewManager
@interface RCTVideoManager : RCTViewManager <RCTBridgeModule>

@end
Loading