From 18e8895712935157de604187fccb937f0a5acd6a Mon Sep 17 00:00:00 2001 From: Nicolas Gonzalez Date: Thu, 25 Oct 2018 08:56:20 -0500 Subject: [PATCH] added video filter --- Video.js | 3 + ios/Video/RCTVideo.m | 192 +++++++++++++++++++++++------------- ios/Video/RCTVideoManager.m | 1 + 3 files changed, 125 insertions(+), 71 deletions(-) diff --git a/Video.js b/Video.js index e430e419..93434b3e 100644 --- a/Video.js +++ b/Video.js @@ -274,7 +274,10 @@ export default class Video extends Component { } } +Video.filterTypes = ['Normal', 'Country', 'Winter', 'Black N White', 'Sunrise', 'Artistic']; + Video.propTypes = { + filter: PropTypes.oneOf(Video.filterTypes), /* Native only */ src: PropTypes.object, seek: PropTypes.oneOfType([ diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index a165d5c8..51ef8949 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -13,6 +13,7 @@ static NSString *const readyForDisplayKeyPath = @"readyForDisplay"; static NSString *const playbackRate = @"rate"; static NSString *const timedMetadata = @"timedMetadata"; static NSString *const externalPlaybackActive = @"externalPlaybackActive"; +static NSDictionary* filters = nil; static int const RCTVideoUnset = -1; @@ -32,22 +33,22 @@ static int const RCTVideoUnset = -1; BOOL _playerLayerObserverSet; RCTVideoPlayerViewController *_playerViewController; NSURL *_videoURL; - + /* Required to publish events */ RCTEventDispatcher *_eventDispatcher; BOOL _playbackRateObserverRegistered; BOOL _isExternalPlaybackActiveObserverRegistered; BOOL _videoLoadStarted; - + bool _pendingSeek; float _pendingSeekTime; float _lastSeekTime; - + /* For sending videoProgress events */ Float64 _progressUpdateInterval; BOOL _controls; id _timeObserver; - + /* Keep track of any modifiers, need to be applied after each play */ float _volume; float _rate; @@ -63,6 +64,7 @@ static int const RCTVideoUnset = -1; BOOL _playWhenInactive; NSString * _ignoreSilentSwitch; NSString * _resizeMode; + NSString * _filter; BOOL _fullscreen; NSString * _fullscreenOrientation; BOOL _fullscreenPlayerPresented; @@ -75,8 +77,18 @@ static int const RCTVideoUnset = -1; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super init])) { + + filters = @{ + @"Normal": @"", + @"Country": @"CISepiaTone", + @"Winter": @"CIPhotoEffectProcess", + @"Black N White": @"CIPhotoEffectNoir", + @"Sunrise": @"CIPhotoEffectTransfer", + @"Artistic": @"CIColorPosterize", + }; + _eventDispatcher = eventDispatcher; - + _playbackRateObserverRegistered = NO; _isExternalPlaybackActiveObserverRegistered = NO; _playbackStalled = NO; @@ -101,23 +113,23 @@ static int const RCTVideoUnset = -1; selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChanged:) name:AVAudioSessionRouteChangeNotification object:nil]; } - + return self; } @@ -127,7 +139,7 @@ static int const RCTVideoUnset = -1; viewController.showsPlaybackControls = YES; viewController.rctDelegate = self; viewController.preferredOrientation = _fullscreenOrientation; - + viewController.view.frame = self.bounds; viewController.player = player; viewController.view.frame = self.bounds; @@ -145,7 +157,7 @@ static int const RCTVideoUnset = -1; { return([playerItem duration]); } - + return(kCMTimeInvalid); } @@ -156,7 +168,7 @@ static int const RCTVideoUnset = -1; { return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue; } - + return (kCMTimeRangeZero); } @@ -197,7 +209,7 @@ static int const RCTVideoUnset = -1; - (void)applicationWillResignActive:(NSNotification *)notification { if (_playInBackground || _playWhenInactive || _paused) return; - + [_player pause]; [_player setRate:0.0]; } @@ -237,18 +249,18 @@ static int const RCTVideoUnset = -1; if (video == nil || video.status != AVPlayerItemStatusReadyToPlay) { return; } - + CMTime playerDuration = [self playerItemDuration]; if (CMTIME_IS_INVALID(playerDuration)) { return; } - + CMTime currentTime = _player.currentTime; const Float64 duration = CMTimeGetSeconds(playerDuration); const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime); - + [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTVideo_progress" object:nil userInfo:@{@"progress": [NSNumber numberWithDouble: currentTimeSecs / duration]}]; - + if( currentTimeSecs >= 0 && self.onVideoProgress) { self.onVideoProgress(@{ @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)], @@ -333,11 +345,11 @@ static int const RCTVideoUnset = -1; [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { _playerItem = playerItem; [self addPlayerItemObservers]; - + [_player pause]; [_playerViewController.view removeFromSuperview]; _playerViewController = nil; - + if (_playbackRateObserverRegistered) { [_player removeObserver:self forKeyPath:playbackRate context:nil]; _playbackRateObserverRegistered = NO; @@ -346,16 +358,16 @@ static int const RCTVideoUnset = -1; [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; _isExternalPlaybackActiveObserverRegistered = NO; } - + _player = [AVPlayer playerWithPlayerItem:_playerItem]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - + [_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; _playbackRateObserverRegistered = YES; - + [_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; _isExternalPlaybackActiveObserverRegistered = YES; - + [self addPlayerTimeObserver]; //Perform on next run loop, otherwise onVideoLoadStart is nil @@ -378,7 +390,7 @@ static int const RCTVideoUnset = -1; if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; } - + // if no file found, check if the file exists in the Document directory NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString* relativeFilePath = [filepath lastPathComponent]; @@ -387,7 +399,7 @@ static int const RCTVideoUnset = -1; if (fileComponents.count > 1) { relativeFilePath = [fileComponents objectAtIndex:1]; } - + NSString *path = [paths.firstObject stringByAppendingPathComponent:relativeFilePath]; if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { return [NSURL fileURLWithPath:path]; @@ -404,21 +416,21 @@ static int const RCTVideoUnset = -1; // sideload text tracks AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; - + AVAssetTrack *videoAsset = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject; AVMutableCompositionTrack *videoCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; [videoCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) ofTrack:videoAsset atTime:kCMTimeZero error:nil]; - + AVAssetTrack *audioAsset = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject; AVMutableCompositionTrack *audioCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; [audioCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) ofTrack:audioAsset atTime:kCMTimeZero error:nil]; - + NSMutableArray* validTextTracks = [NSMutableArray array]; for (int i = 0; i < _textTracks.count; ++i) { AVURLAsset *textURLAsset; @@ -457,7 +469,7 @@ static int const RCTVideoUnset = -1; ? [NSURL URLWithString:uri] : [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc] init]; - + if (isNetwork) { /* Per #1091, this is not a public API. * We need to either get approval from Apple to use this or use a different approach. @@ -560,40 +572,40 @@ static int const RCTVideoUnset = -1; for (AVMetadataItem *item in items) { NSString *value = (NSString *)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 *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)) @@ -603,7 +615,7 @@ static int const RCTVideoUnset = -1; orientation = @"portrait"; } } - + if (self.onVideoLoad && _videoLoadStarted) { self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration], @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)], @@ -623,7 +635,7 @@ static int const RCTVideoUnset = -1; @"target": self.reactTag}); } _videoLoadStarted = NO; - + [self attachListeners]; [self applyModifiers]; } else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) { @@ -683,7 +695,7 @@ static int const RCTVideoUnset = -1; selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:[_player currentItem]]; - + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:nil]; @@ -706,7 +718,7 @@ static int const RCTVideoUnset = -1; if(self.onVideoEnd) { self.onVideoEnd(@{@"target": self.reactTag}); } - + if (_repeat) { AVPlayerItem *item = [notification object]; [item seekToTime:kCMTimeZero]; @@ -767,7 +779,7 @@ static int const RCTVideoUnset = -1; [_player play]; [_player setRate:_rate]; } - + _paused = paused; } @@ -789,19 +801,19 @@ static int const RCTVideoUnset = -1; { NSNumber *seekTime = info[@"time"]; NSNumber *seekTolerance = info[@"tolerance"]; - + int timeScale = 1000; - + AVPlayerItem *item = _player.currentItem; if (item && item.status == AVPlayerItemStatusReadyToPlay) { // TODO check loadedTimeRanges - + CMTime cmSeekTime = CMTimeMakeWithSeconds([seekTime floatValue], timeScale); CMTime current = item.currentTime; // TODO figure out a good tolerance level CMTime tolerance = CMTimeMake([seekTolerance floatValue], timeScale); BOOL wasPaused = _paused; - + if (CMTimeCompare(current, cmSeekTime) != 0) { if (!wasPaused) [_player pause]; [_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) { @@ -817,10 +829,10 @@ static int const RCTVideoUnset = -1; @"target": self.reactTag}); } }]; - + _pendingSeek = false; } - + } else { // TODO: See if this makes sense and if so, actually implement it _pendingSeek = true; @@ -855,12 +867,13 @@ static int const RCTVideoUnset = -1; [_player setVolume:_volume]; [_player setMuted:NO]; } - + [self setSelectedAudioTrack:_selectedAudioTrack]; [self setSelectedTextTrack:_selectedTextTrack]; [self setResizeMode:_resizeMode]; [self setRepeat:_repeat]; [self setPaused:_paused]; + [self setFilter:_filter]; [self setControls:_controls]; [self setAllowsExternalPlayback:_allowsExternalPlayback]; } @@ -876,7 +889,7 @@ static int const RCTVideoUnset = -1; AVMediaSelectionGroup *group = [_player.currentItem.asset mediaSelectionGroupForMediaCharacteristic:characteristic]; AVMediaSelectionOption *mediaOption; - + if ([type isEqualToString:@"disabled"]) { // Do nothing. We want to ensure option is nil } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { @@ -909,7 +922,7 @@ static int const RCTVideoUnset = -1; [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; return; } - + // If a match isn't found, option will be nil and text tracks will be disabled [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } @@ -933,7 +946,7 @@ static int const RCTVideoUnset = -1; - (void) setSideloadedText { NSString *type = _selectedTextTrack[@"type"]; NSArray *textTracks = [self getTextTrackInfo]; - + // The first few tracks will be audio & video track int firstTextIndex = 0; for (firstTextIndex = 0; firstTextIndex < _player.currentItem.tracks.count; ++firstTextIndex) { @@ -941,9 +954,9 @@ static int const RCTVideoUnset = -1; break; } } - + int selectedTrackIndex = RCTVideoUnset; - + if ([type isEqualToString:@"disabled"]) { // Do nothing. We want to ensure option is nil } else if ([type isEqualToString:@"language"]) { @@ -972,7 +985,7 @@ static int const RCTVideoUnset = -1; } } } - + // in the situation that a selected text track is not available (eg. specifies a textTrack not available) if (![type isEqualToString:@"disabled"] && selectedTrackIndex == RCTVideoUnset) { CFArrayRef captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser); @@ -989,7 +1002,7 @@ static int const RCTVideoUnset = -1; } } } - + for (int i = firstTextIndex; i < _player.currentItem.tracks.count; ++i) { BOOL isEnabled = NO; if (selectedTrackIndex != RCTVideoUnset) { @@ -1004,7 +1017,7 @@ static int const RCTVideoUnset = -1; AVMediaSelectionGroup *group = [_player.currentItem.asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; AVMediaSelectionOption *mediaOption; - + if ([type isEqualToString:@"disabled"]) { // Do nothing. We want to ensure option is nil } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { @@ -1037,7 +1050,7 @@ static int const RCTVideoUnset = -1; [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; return; } - + // If a match isn't found, option will be nil and text tracks will be disabled [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } @@ -1045,7 +1058,7 @@ static int const RCTVideoUnset = -1; - (void)setTextTracks:(NSArray*) textTracks; { _textTracks = textTracks; - + // in case textTracks was set after selectedTextTrack if (_selectedTextTrack) [self setSelectedTextTrack:_selectedTextTrack]; } @@ -1077,7 +1090,7 @@ static int const RCTVideoUnset = -1; { // if sideloaded, textTracks will already be set if (_textTracks) return _textTracks; - + // if streaming video, we extract the text tracks NSMutableArray *textTracks = [[NSMutableArray alloc] init]; AVMediaSelectionGroup *group = [_player.currentItem.asset @@ -1115,7 +1128,7 @@ static int const RCTVideoUnset = -1; } // Set presentation style to fullscreen [_playerViewController setModalPresentationStyle:UIModalPresentationFullScreen]; - + // Find the nearest view controller UIViewController *viewController = [self firstAvailableUIViewController]; if( !viewController ) @@ -1151,6 +1164,43 @@ static int const RCTVideoUnset = -1; } } +- (void)setFilter:(NSString *)filter { + + _filter = filter; + + AVAsset *asset = _playerItem.asset; + + if (asset != nil) { + + NSString *filterName = filters[filter]; + 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]; + + } + + }]; + + } + +} + - (void)setFullscreenOrientation:(NSString *)orientation { _fullscreenOrientation = orientation; if (_fullscreenPlayerPresented) { @@ -1177,13 +1227,13 @@ static int const RCTVideoUnset = -1; _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; _playerLayer.frame = self.bounds; _playerLayer.needsDisplayOnBoundsChange = YES; - + // to prevent video from being animated when resizeMode is 'cover' // resize mode must be set before layer is added [self setResizeMode:_resizeMode]; [_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; _playerLayerObserverSet = YES; - + [self.layer addSublayer:_playerLayer]; self.layer.needsDisplayOnBoundsChange = YES; } @@ -1211,7 +1261,7 @@ static int const RCTVideoUnset = -1; - (void)setProgressUpdateInterval:(float)progressUpdateInterval { _progressUpdateInterval = progressUpdateInterval; - + if (_timeObserver) { [self removePlayerTimeObserver]; [self addPlayerTimeObserver]; @@ -1262,7 +1312,7 @@ static int const RCTVideoUnset = -1; { [self setControls:true]; } - + if( _controls ) { view.frame = self.bounds; @@ -1294,7 +1344,7 @@ static int const RCTVideoUnset = -1; if( _controls ) { _playerViewController.view.frame = self.bounds; - + // also adjust all subviews of contentOverlayView for (UIView* subview in _playerViewController.contentOverlayView.subviews) { subview.frame = self.bounds; @@ -1323,18 +1373,18 @@ static int const RCTVideoUnset = -1; _isExternalPlaybackActiveObserverRegistered = NO; } _player = nil; - + [self removePlayerLayer]; - + [_playerViewController.view removeFromSuperview]; _playerViewController = nil; - + [self removePlayerTimeObserver]; [self removePlayerItemObservers]; - + _eventDispatcher = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; - + [super removeFromSuperview]; } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index aa3c4670..055d2213 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -38,6 +38,7 @@ RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(currentTime, float); RCT_EXPORT_VIEW_PROPERTY(fullscreen, 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);