#import "TGVideoMessageCaptureController.h" #import "LegacyComponentsInternal.h" #import #import #import #import #import #import #import #import #import #import "TGVideoCameraPipeline.h" #import #import #import #import #import #import #import "TGColor.h" #import "TGImageUtils.h" #import "TGMediaPickerSendActionSheetController.h" #import "TGOverlayControllerWindow.h" #import const NSTimeInterval TGVideoMessageMaximumDuration = 60.0; typedef enum { TGVideoMessageTransitionTypeUsual, TGVideoMessageTransitionTypeSimplified, TGVideoMessageTransitionTypeLegacy } TGVideoMessageTransitionType; @interface TGVideoMessageCaptureControllerWindow : TGOverlayControllerWindow @property (nonatomic, assign) CGRect controlsFrame; @property (nonatomic, assign) bool locked; @end @implementation TGVideoMessageCaptureControllerAssets - (instancetype)initWithSendImage:(UIImage *)sendImage slideToCancelImage:(UIImage *)slideToCancelImage actionDelete:(UIImage *)actionDelete { self = [super init]; if (self != nil) { _sendImage = sendImage; _slideToCancelImage = slideToCancelImage; _actionDelete = actionDelete; } return self; } @end @interface TGVideoMessageCaptureController () { SQueue *_queue; AVCaptureDevicePosition _preferredPosition; TGVideoCameraPipeline *_capturePipeline; NSURL *_url; PGCameraVolumeButtonHandler *_buttonHandler; bool _forStory; bool _autorotationWasEnabled; bool _dismissed; bool _gpuAvailable; bool _locked; bool _positionChangeLocked; bool _alreadyStarted; CGRect _controlsFrame; TGVideoMessageControls *_controlsView; TGModernButton *_switchButton; UIView *_wrapperView; UIView *_blurView; UIView *_fadeView; UIView *_circleWrapperView; UIImageView *_shadowView; UIView *_circleView; TGVideoCameraGLView *_previewView; TGVideoMessageRingView *_ringView; UIPinchGestureRecognizer *_pinchGestureRecognizer; UIView *_separatorView; UIImageView *_placeholderView; TGVideoMessageShimmerView *_shimmerView; bool _automaticDismiss; NSTimeInterval _startTimestamp; NSTimer *_recordingTimer; NSTimeInterval _previousDuration; NSUInteger _audioRecordingDurationSeconds; NSUInteger _audioRecordingDurationMilliseconds; id _activityHolder; SMetaDisposable *_activityDisposable; SMetaDisposable *_currentAudioSession; bool _otherAudioPlaying; id _didEnterBackgroundObserver; bool _stopped; id _liveUploadData; UIImage *_thumbnailImage; NSDictionary *_thumbnails; NSTimeInterval _duration; AVPlayer *_player; id _didPlayToEndObserver; TGModernGalleryVideoView *_videoView; UIImageView *_muteView; bool _muted; SMetaDisposable *_thumbnailsDisposable; id _context; UIView *(^_transitionInView)(); id _liveUploadInterface; int32_t _slowmodeTimestamp; UIView * (^_slowmodeView)(void); TGVideoMessageCaptureControllerAssets *_assets; bool _canSendSilently; bool _canSchedule; bool _reminder; UIImpactFeedbackGenerator *_generator; } @property (nonatomic, copy) bool(^isAlreadyLocked)(void); @end @implementation TGVideoMessageCaptureController - (instancetype)initWithContext:(id)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder { self = [super initWithContext:context]; if (self != nil) { _context = context; _forStory = forStory; _transitionInView = [transitionInView copy]; self.isAlreadyLocked = isAlreadyLocked; _liveUploadInterface = liveUploadInterface; _assets = assets; _pallete = pallete; _canSendSilently = canSendSilently; _canSchedule = canSchedule; _reminder = reminder; _slowmodeTimestamp = slowmodeTimestamp; _slowmodeView = [slowmodeView copy]; _url = [TGVideoMessageCaptureController tempOutputPath]; _queue = [[SQueue alloc] init]; _previousDuration = 0.0; _preferredPosition = AVCaptureDevicePositionFront; self.isImportant = true; _controlsFrame = controlsFrame; _gpuAvailable = true; _activityDisposable = [[SMetaDisposable alloc] init]; _currentAudioSession = [[SMetaDisposable alloc] init]; __weak TGVideoMessageCaptureController *weakSelf = self; _didEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:nil usingBlock:^(__unused NSNotification *notification) { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil && !strongSelf->_stopped) { strongSelf->_automaticDismiss = true; strongSelf->_gpuAvailable = false; [strongSelf dismiss:true]; } }]; _thumbnailsDisposable = [[SMetaDisposable alloc] init]; TGVideoMessageCaptureControllerWindow *window = [[TGVideoMessageCaptureControllerWindow alloc] initWithManager:[_context makeOverlayWindowManager] parentController:parentController contentController:self keepKeyboard:true]; window.windowLevel = 1000000000.0f - 0.001f; window.hidden = false; window.controlsFrame = controlsFrame; } return self; } - (void)dealloc { printf("Video controller dealloc\n"); [_thumbnailsDisposable dispose]; [[NSNotificationCenter defaultCenter] removeObserver:_didEnterBackgroundObserver]; [_activityDisposable dispose]; id currentAudioSession = _currentAudioSession; [_queue dispatch:^{ [currentAudioSession dispose]; }]; } + (NSURL *)tempOutputPath { return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"cam_%x.mp4", (int)arc4random()]]]; } - (void)setPallete:(TGModernConversationInputMicPallete *)pallete { _pallete = pallete; if (!_alreadyStarted) return; TGVideoMessageTransitionType type = [self _transitionType]; if (type != TGVideoMessageTransitionTypeLegacy && ((UIVisualEffectView *)_blurView).effect != nil) { UIBlurEffect *effect = [UIBlurEffect effectWithStyle:self.pallete.isDark ? UIBlurEffectStyleDark : UIBlurEffectStyleLight]; ((UIVisualEffectView *)_blurView).effect = effect; } UIColor *curtainColor = [UIColor whiteColor]; if (self.pallete != nil && self.pallete.isDark) curtainColor = [UIColor blackColor]; _fadeView.backgroundColor = [curtainColor colorWithAlphaComponent:0.4f]; _ringView.accentColor = self.pallete != nil ? self.pallete.buttonColor : TGAccentColor(); _controlsView.pallete = self.pallete; _separatorView.backgroundColor = self.pallete != nil ? self.pallete.borderColor : UIColorRGB(0xb2b2b2); UIImage *switchImage = TGComponentsImageNamed(@"VideoRecordPositionSwitch"); if (self.pallete != nil) switchImage = TGTintedImage(switchImage, self.pallete.buttonColor); [_switchButton setImage:switchImage forState:UIControlStateNormal]; } - (void)loadView { [super loadView]; self.view = [[TGPhotoEditorSparseView alloc] initWithFrame:self.view.frame]; self.view.backgroundColor = [UIColor clearColor]; CGFloat bottomOffset = self.view.frame.size.height - CGRectGetMaxY(_controlsFrame); if (bottomOffset > 44.0) { bottomOffset = 0.0f; } CGRect wrapperFrame = TGIsPad() || _forStory ? CGRectMake(0.0f, 0.0f, self.view.frame.size.width, CGRectGetMaxY(_controlsFrame) + bottomOffset) : CGRectMake(0.0f, 0.0f, self.view.frame.size.width, CGRectGetMinY(_controlsFrame)); _wrapperView = [[TGPhotoEditorSparseView alloc] initWithFrame:wrapperFrame]; _wrapperView.clipsToBounds = true; [self.view addSubview:_wrapperView]; UIColor *curtainColor = [UIColor whiteColor]; if (self.pallete != nil && self.pallete.isDark) curtainColor = [UIColor blackColor]; TGVideoMessageTransitionType type = [self _transitionType]; CGRect fadeFrame = CGRectMake(0.0f, 0.0f, _wrapperView.frame.size.width, _wrapperView.frame.size.height); if (type != TGVideoMessageTransitionTypeLegacy) { UIBlurEffect *effect = nil; if (type == TGVideoMessageTransitionTypeSimplified) effect = [UIBlurEffect effectWithStyle:self.pallete.isDark ? UIBlurEffectStyleDark : UIBlurEffectStyleLight]; _blurView = [[UIVisualEffectView alloc] initWithEffect:effect]; if (!_forStory) { [_wrapperView addSubview:_blurView]; } if (type == TGVideoMessageTransitionTypeSimplified) { _blurView.alpha = 0.0f; } else { _fadeView = [[UIView alloc] initWithFrame:fadeFrame]; _fadeView.alpha = 0.0f; _fadeView.backgroundColor = [curtainColor colorWithAlphaComponent:0.4f]; if (!_forStory) { [_wrapperView addSubview:_fadeView]; } } } else { _fadeView = [[UIView alloc] initWithFrame:fadeFrame]; _fadeView.alpha = 0.0f; _fadeView.backgroundColor = [curtainColor colorWithAlphaComponent:0.6f]; if (!_forStory) { [_wrapperView addSubview:_fadeView]; } } CGFloat minSide = MIN(_wrapperView.frame.size.width, _wrapperView.frame.size.height); bool isSE = _wrapperView.frame.size.width == 320.0 || _wrapperView.frame.size.height == 320.0; CGFloat diameter = isSE ? 216.0 : MIN(404.0, minSide - 24.0f); CGFloat shadowSize = 21.0f; CGFloat circleWrapperViewLength = diameter + shadowSize * 2.0; _circleWrapperView = [[UIView alloc] initWithFrame:(CGRect){ .origin.x = (_wrapperView.bounds.size.width - circleWrapperViewLength) / 2.0f, .origin.y = _wrapperView.bounds.size.height + circleWrapperViewLength * 0.3f, .size.width = circleWrapperViewLength, .size.height = circleWrapperViewLength }]; _circleWrapperView.alpha = 0.0f; _circleWrapperView.clipsToBounds = false; [_wrapperView addSubview:_circleWrapperView]; _shadowView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"VideoMessageShadow")]; _shadowView.frame = _circleWrapperView.bounds; [_circleWrapperView addSubview:_shadowView]; _circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, shadowSize, shadowSize)]; _circleView.clipsToBounds = true; _circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f; [_circleWrapperView addSubview:_circleView]; _placeholderView = [[UIImageView alloc] initWithFrame:_circleView.bounds]; _placeholderView.backgroundColor = [UIColor blackColor]; _placeholderView.image = [TGVideoMessageCaptureController startImage]; [_circleView addSubview:_placeholderView]; _shimmerView = [[TGVideoMessageShimmerView alloc] initWithFrame:_circleView.bounds]; [_shimmerView updateAbsoluteRect:_circleView.bounds containerSize:_circleView.bounds.size]; [_circleView addSubview:_shimmerView]; if (@available(iOS 11.0, *)) { _shadowView.accessibilityIgnoresInvertColors = true; _placeholderView.accessibilityIgnoresInvertColors = true; } CGFloat ringViewLength = diameter - 8.0f; _ringView = [[TGVideoMessageRingView alloc] initWithFrame:(CGRect){ .origin.x = (_circleWrapperView.bounds.size.width - ringViewLength) / 2.0f, .origin.y = (_circleWrapperView.bounds.size.height - ringViewLength) / 2.0f, .size.width = ringViewLength, .size.height = ringViewLength }]; _ringView.accentColor = [UIColor colorWithWhite:1.0 alpha:0.6]; [_circleWrapperView addSubview:_ringView]; CGRect controlsFrame = _controlsFrame; _controlsView = [[TGVideoMessageControls alloc] initWithFrame:controlsFrame forStory:_forStory assets:_assets slowmodeTimestamp:_slowmodeTimestamp slowmodeView:_slowmodeView]; _controlsView.pallete = self.pallete; _controlsView.clipsToBounds = true; _controlsView.parent = self; _controlsView.isAlreadyLocked = self.isAlreadyLocked; _controlsView.controlsHeight = _controlsFrame.size.height; __weak TGVideoMessageCaptureController *weakSelf = self; _controlsView.cancel = ^ { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_automaticDismiss = true; [strongSelf dismiss:true]; if (strongSelf.onCancel != nil) strongSelf.onCancel(); } }; _controlsView.deletePressed = ^ { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_automaticDismiss = true; [strongSelf dismiss:true]; if (strongSelf.onCancel != nil) strongSelf.onCancel(); }; }; _controlsView.sendPressed = ^bool { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil) { return [strongSelf sendPressed]; } else { return false; } }; _controlsView.sendLongPressed = ^bool{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf sendLongPressed]; } return true; }; [self.view addSubview:_controlsView]; _separatorView = [[UIView alloc] initWithFrame:CGRectMake(controlsFrame.origin.x, controlsFrame.origin.y - TGScreenPixel, controlsFrame.size.width, TGScreenPixel)]; _separatorView.backgroundColor = self.pallete != nil ? self.pallete.borderColor : UIColorRGB(0xb2b2b2); _separatorView.userInteractionEnabled = false; if (!_forStory) { [self.view addSubview:_separatorView]; } if ([TGVideoCameraPipeline cameraPositionChangeAvailable]) { UIImage *switchImage = TGComponentsImageNamed(@"VideoRecordPositionSwitch"); if (self.pallete != nil) switchImage = TGTintedImage(switchImage, self.pallete.buttonColor); _switchButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 44.0f, 44.0f)]; _switchButton.alpha = 0.0f; _switchButton.adjustsImageWhenHighlighted = false; _switchButton.adjustsImageWhenDisabled = false; [_switchButton setImage:switchImage forState:UIControlStateNormal]; [_switchButton addTarget:self action:@selector(changeCameraPosition) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_switchButton]; } _pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)]; _pinchGestureRecognizer.delegate = self; [self.view addGestureRecognizer:_pinchGestureRecognizer]; void (^voidBlock)(void) = ^{}; _buttonHandler = [[PGCameraVolumeButtonHandler alloc] initWithUpButtonPressedBlock:voidBlock upButtonReleasedBlock:voidBlock downButtonPressedBlock:voidBlock downButtonReleasedBlock:voidBlock]; [self configureCamera]; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if (gestureRecognizer == _pinchGestureRecognizer) return _capturePipeline.isZoomAvailable; return true; } - (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer { switch (gestureRecognizer.state) { case UIGestureRecognizerStateChanged: { CGFloat delta = (gestureRecognizer.scale - 1.0f) / 1.5f; CGFloat value = MAX(0.0f, MIN(1.0f, _capturePipeline.zoomLevel + delta)); [_capturePipeline setZoomLevel:value]; gestureRecognizer.scale = 1.0f; } break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: [_capturePipeline cancelZoom]; break; default: break; } } - (TGVideoMessageTransitionType)_transitionType { static dispatch_once_t onceToken; static TGVideoMessageTransitionType type; dispatch_once(&onceToken, ^ { CGSize screenSize = TGScreenSize(); if (iosMajorVersion() < 8 || (NSInteger)screenSize.height == 480) type = TGVideoMessageTransitionTypeLegacy; else if (iosMajorVersion() == 8) type = TGVideoMessageTransitionTypeSimplified; else type = TGVideoMessageTransitionTypeUsual; }); return type; } - (void)setupPreviewView { _previewView = [[TGVideoCameraGLView alloc] initWithFrame:_circleView.bounds]; [_circleView insertSubview:_previewView belowSubview:_placeholderView]; if (@available(iOS 11.0, *)) { _previewView.accessibilityIgnoresInvertColors = true; } [self captureStarted]; } - (void)_transitionIn { TGVideoMessageTransitionType type = [self _transitionType]; if (type == TGVideoMessageTransitionTypeUsual) { UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; UIView *rootView = _transitionInView(); rootView.superview.backgroundColor = [UIColor whiteColor]; [UIView animateWithDuration:0.22 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^ { ((UIVisualEffectView *)_blurView).effect = effect; _fadeView.alpha = 1.0f; } completion:nil]; } else if (type == TGVideoMessageTransitionTypeSimplified) { [UIView animateWithDuration:0.22 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^ { _blurView.alpha = 1.0f; } completion:nil]; } else { [UIView animateWithDuration:0.25 animations:^ { _fadeView.alpha = 1.0f; }]; } } - (void)_transitionOut { TGVideoMessageTransitionType type = [self _transitionType]; if (type == TGVideoMessageTransitionTypeUsual) { [UIView animateWithDuration:0.22 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^ { ((UIVisualEffectView *)_blurView).effect = nil; _fadeView.alpha = 0.0f; } completion:nil]; } else if (type == TGVideoMessageTransitionTypeSimplified) { [UIView animateWithDuration:0.22 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^ { _blurView.alpha = 0.0f; } completion:nil]; } else { [UIView animateWithDuration:0.15 animations:^ { _fadeView.alpha = 0.0f; }]; } } - (void)viewWillAppear:(BOOL)animated { if (self.ignoreAppearEvents) { return; } [super viewWillAppear:animated]; _capturePipeline.renderingEnabled = true; _startTimestamp = CFAbsoluteTimeGetCurrent(); [_controlsView setShowRecordingInterface:true velocity:0.0f]; [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:true]; [self _transitionIn]; [self _beginAudioSession:false]; } - (void)viewDidAppear:(BOOL)animated { if (self.ignoreAppearEvents) { return; } [super viewDidAppear:animated]; _autorotationWasEnabled = [TGViewController autorotationAllowed]; [TGViewController disableAutorotation]; _circleWrapperView.transform = CGAffineTransformMakeScale(0.3f, 0.3f); CGPoint targetPosition = (CGPoint){ .x = _wrapperView.frame.size.width / 2.0f, .y = _wrapperView.frame.size.height / 2.0f - _controlsView.frame.size.height }; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" switch (self.interfaceOrientation) { case UIInterfaceOrientationLandscapeLeft: break; case UIInterfaceOrientationLandscapeRight: break; default: if (self.view.frame.size.height > self.view.frame.size.width && fabs(_wrapperView.frame.size.height - self.view.frame.size.height) < 50.0f) targetPosition.y = _wrapperView.frame.size.height / 3.0f - 20.0f; CGFloat minY = _circleWrapperView.bounds.size.height / 2.0f + 40.0f; if (fabs(_wrapperView.frame.size.height - self.view.frame.size.height) > 50.0 && _wrapperView.frame.size.width == 320.0) { minY = _circleWrapperView.bounds.size.height / 2.0f + 4.0; } targetPosition.y = MAX(minY, targetPosition.y); break; } #pragma clang diagnostic pop if (TGIsPad()) { _circleWrapperView.center = targetPosition; } [UIView animateWithDuration:0.5 delay:0.0 usingSpringWithDamping:0.8f initialSpringVelocity:0.2f options:kNilOptions animations:^ { if (!TGIsPad()) { _circleWrapperView.center = targetPosition; } _circleWrapperView.transform = CGAffineTransformIdentity; } completion:nil]; [UIView animateWithDuration:0.2 animations:^ { _circleWrapperView.alpha = 1.0f; } completion:nil]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; CGRect fadeFrame = TGIsPad() || _forStory ? self.view.bounds : CGRectMake(0.0f, 0.0f, _wrapperView.frame.size.width, _wrapperView.frame.size.height); _blurView.frame = fadeFrame; } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)__unused toInterfaceOrientation duration:(NSTimeInterval)__unused duration { if (TGIsPad()) { _automaticDismiss = true; [self dismiss:true]; } } - (void)dismissImmediately { [super dismiss]; [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:false]; [self stopCapture]; [self _endAudioSession]; if (_autorotationWasEnabled) [TGViewController enableAutorotation]; if (_didDismiss) { _didDismiss(); } } - (void)dismiss { [self dismiss:true]; } - (void)dismiss:(bool)cancelled { _dismissed = cancelled; if (self.onDismiss != nil) self.onDismiss(_automaticDismiss, cancelled); if (_player != nil) [_player pause]; self.view.backgroundColor = [UIColor clearColor]; self.view.userInteractionEnabled = false; _circleWrapperView.layer.allowsGroupOpacity = true; [UIView animateWithDuration:0.15 animations:^ { _circleWrapperView.alpha = 0.0f; _switchButton.alpha = 0.0f; }]; [self _transitionOut]; [_controlsView setShowRecordingInterface:false velocity:0.0f]; TGDispatchAfter(0.3, dispatch_get_main_queue(), ^ { [self dismissImmediately]; }); } - (void)complete { if (_stopped) return; [_activityDisposable dispose]; [self stopRecording:^() { TGDispatchOnMainThread(^{ //[self dismiss:false]; [self description]; }); }]; } - (void)buttonInteractionUpdate:(CGPoint)value { [_controlsView buttonInteractionUpdate:value]; } - (void)setLocked { if ([self.view.window isKindOfClass:[TGVideoMessageCaptureControllerWindow class]]) { ((TGVideoMessageCaptureControllerWindow *)self.view.window).locked = true; } [_controlsView setLocked]; } - (CGRect)frameForSendButton { return [_controlsView convertRect:[_controlsView frameForSendButton] toView:self.view]; } - (bool)stop { if (!_capturePipeline.isRecording) return false; if (_capturePipeline.videoDuration < 0.33) return false; if ([self.view.window isKindOfClass:[TGVideoMessageCaptureControllerWindow class]]) { ((TGVideoMessageCaptureControllerWindow *)self.view.window).locked = false; } _stopped = true; _gpuAvailable = false; _switchButton.userInteractionEnabled = false; [_activityDisposable dispose]; [self stopRecording:^{}]; if (self.didStop != nil) { self.didStop(); } return true; } - (void)send { [self sendPressed]; } - (bool)sendPressed { if (_slowmodeTimestamp != 0) { int32_t timestamp = (int32_t)[[NSDate date] timeIntervalSince1970]; if (timestamp < _slowmodeTimestamp) { if (_displaySlowmodeTooltip) { _displaySlowmodeTooltip(); } return false; } } [self finishWithURL:_url dimensions:CGSizeMake(240.0f, 240.0f) duration:_duration liveUploadData:_liveUploadData thumbnailImage:_thumbnailImage isSilent:false scheduleTimestamp:0]; _automaticDismiss = true; [self dismiss:false]; return true; } - (void)sendLongPressed { if (iosMajorVersion() >= 10) { if (_generator == nil) { _generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; } [_generator impactOccurred]; } TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:_context isDark:self.pallete.isDark sendButtonFrame:[_controlsView convertRect:[_controlsView frameForSendButton] toView:nil] canSendSilently:_canSendSilently canSendWhenOnline:false canSchedule:_canSchedule reminder:_reminder hasTimer:false]; __weak TGVideoMessageCaptureController *weakSelf = self; controller.send = ^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:0]; _automaticDismiss = true; [strongSelf dismiss:false]; }; controller.sendSilently = ^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:true scheduleTimestamp:0]; _automaticDismiss = true; [strongSelf dismiss:false]; }; controller.sendWhenOnline = ^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:0x7ffffffe]; _automaticDismiss = true; [strongSelf dismiss:false]; }; controller.schedule = ^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } if (strongSelf.presentScheduleController) { strongSelf.presentScheduleController(^(int32_t time) { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:time]; _automaticDismiss = true; [strongSelf dismiss:false]; }); } }; TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[_context makeOverlayWindowManager] parentController:self contentController:controller]; controllerWindow.hidden = false; } - (void)unmutePressed { [self _updateMuted:false]; [[SQueue concurrentDefaultQueue] dispatch:^ { _player.muted = false; [self _seekToPosition:_controlsView.scrubberView.trimStartValue]; }]; } - (void)_stop { [_controlsView setStopped]; [UIView animateWithDuration:0.2 animations:^ { _switchButton.alpha = 0.0f; _ringView.alpha = 0.0f; } completion:^(__unused BOOL finished) { _ringView.hidden = true; _switchButton.hidden = true; }]; } - (UIImage *)systemUnmuteButton { static UIImage *image = nil; if (image == nil) { UIGraphicsBeginImageContextWithOptions(CGSizeMake(24.0f, 24.0f), false, 0.0f); CGContextRef context = UIGraphicsGetCurrentContext(); UIColor *color = UIColorRGBA(0x000000, 0.4f); CGContextSetFillColorWithColor(context, color.CGColor); CGContextFillEllipseInRect(context, CGRectMake(0.0f, 0.0f, 24.0f, 24.0f)); UIImage *iconImage = TGComponentsImageNamed(@"VideoMessageMutedIcon.png"); [iconImage drawAtPoint:CGPointMake(CGFloor((24.0f - iconImage.size.width) / 2.0f), CGFloor((24.0f - iconImage.size.height) / 2.0f))]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } return image; } - (void)setupVideoView { _controlsView.scrubberView.trimStartValue = 0.0; _controlsView.scrubberView.trimEndValue = _duration; [_controlsView.scrubberView setTrimApplied:false]; [_controlsView.scrubberView reloadData]; _player = [[AVPlayer alloc] initWithURL:_url]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; _player.muted = true; _didPlayToEndObserver = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(playerItemDidPlayToEndTime:) name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem]; _videoView = [[TGModernGalleryVideoView alloc] initWithFrame: CGRectInset(_previewView.frame, -3.0, -3.0) player:_player]; [_previewView.superview insertSubview:_videoView belowSubview:_previewView]; UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(unmutePressed)]; [_videoView addGestureRecognizer:gestureRecognizer]; _muted = true; _muteView = [[UIImageView alloc] initWithImage:[self systemUnmuteButton]]; _muteView.frame = CGRectMake(floor(CGRectGetMidX(_circleView.bounds) - 12.0f), CGRectGetMaxY(_circleView.bounds) - 24.0f - 8.0f, 24.0f, 24.0f); [_previewView.superview addSubview:_muteView]; [_player play]; [UIView animateWithDuration:0.1 delay:0.1 options:kNilOptions animations:^ { _previewView.alpha = 0.0f; } completion:nil]; } - (void)_updateMuted:(bool)muted { if (muted == _muted) return; _muted = muted; UIView *muteButtonView = _muteView; [muteButtonView.layer removeAllAnimations]; if ((muteButtonView.transform.a < 0.3f || muteButtonView.transform.a > 1.0f) || muteButtonView.alpha < FLT_EPSILON) { muteButtonView.transform = CGAffineTransformMakeScale(0.001f, 0.001f); muteButtonView.alpha = 0.0f; } [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState | 7 << 16 animations:^ { muteButtonView.transform = muted ? CGAffineTransformIdentity : CGAffineTransformMakeScale(0.001f, 0.001f); } completion:nil]; [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^ { muteButtonView.alpha = muted ? 1.0f : 0.0f; } completion:nil]; } - (void)_seekToPosition:(NSTimeInterval)position { CMTime targetTime = CMTimeMakeWithSeconds(MIN(position, _duration - 0.1), NSEC_PER_SEC); [_player.currentItem seekToTime:targetTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; } - (void)playerItemDidPlayToEndTime:(NSNotification *)__unused notification { [self _seekToPosition:_controlsView.scrubberView.trimStartValue]; TGDispatchOnMainThread(^ { [self _updateMuted:true]; [[SQueue concurrentDefaultQueue] dispatch:^ { _player.muted = true; }]; }); } #pragma mark - - (void)changeCameraPosition { if (_positionChangeLocked) return; _preferredPosition = (_preferredPosition == AVCaptureDevicePositionFront) ? AVCaptureDevicePositionBack : AVCaptureDevicePositionFront; _gpuAvailable = false; [_previewView removeFromSuperview]; _previewView = nil; _ringView.alpha = 0.0f; dispatch_async(dispatch_get_main_queue(), ^ { [UIView transitionWithView:_circleWrapperView duration:0.4f options:UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionCurveEaseOut animations:^ { _placeholderView.hidden = false; } completion:^(__unused BOOL finished) { _ringView.alpha = 1.0f; _gpuAvailable = true; }]; [_capturePipeline setCameraPosition:_preferredPosition]; _positionChangeLocked = true; TGDispatchAfter(1.0, dispatch_get_main_queue(), ^ { _positionChangeLocked = false; }); }); } #pragma mark - - (void)startRecording { [_buttonHandler ignoreEventsFor:1.0f andDisable:false]; [_capturePipeline startRecording:_url preset:TGMediaVideoConversionPresetVideoMessage liveUpload:true]; [self startRecordingTimer]; } - (void)stopRecording:(void (^)())completed { __weak TGVideoMessageCaptureController *weakSelf = self; [_capturePipeline stopRecording:^(bool success) { TGDispatchOnMainThread(^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } if (!success) { if (!strongSelf->_dismissed && strongSelf.finishedWithVideo != nil) { strongSelf.finishedWithVideo(nil, nil, 0, 0.0, CGSizeZero, nil, nil, false, 0); } } }); }]; [_buttonHandler ignoreEventsFor:1.0f andDisable:true]; [_capturePipeline stopRunning]; } - (void)finishWithURL:(NSURL *)url dimensions:(CGSize)dimensions duration:(NSTimeInterval)duration liveUploadData:(id )liveUploadData thumbnailImage:(UIImage *)thumbnailImage isSilent:(bool)isSilent scheduleTimestamp:(int32_t)scheduleTimestamp { if (duration < 1.0) _dismissed = true; CGFloat minSize = MIN(thumbnailImage.size.width, thumbnailImage.size.height); CGFloat maxSize = MAX(thumbnailImage.size.width, thumbnailImage.size.height); bool mirrored = true; UIImageOrientation orientation = [self orientationForThumbnailWithTransform:_capturePipeline.videoTransform mirrored:mirrored]; UIImage *image = TGPhotoEditorCrop(thumbnailImage, nil, orientation, 0.0f, CGRectMake((maxSize - minSize) / 2.0f, 0.0f, minSize, minSize), mirrored, CGSizeMake(240.0f, 240.0f), thumbnailImage.size, true); NSDictionary *fileDictionary = [[NSFileManager defaultManager] attributesOfItemAtPath:url.path error:NULL]; NSUInteger fileSize = (NSUInteger)[fileDictionary fileSize]; UIImage *startImage = TGSecretBlurredAttachmentImage(image, image.size, NULL, false, 0); [TGVideoMessageCaptureController saveStartImage:startImage]; TGVideoEditAdjustments *adjustments = nil; if (_stopped) { NSTimeInterval trimStartValue = _controlsView.scrubberView.trimStartValue; NSTimeInterval trimEndValue = _controlsView.scrubberView.trimEndValue; if (trimStartValue > DBL_EPSILON || trimEndValue < _duration - DBL_EPSILON) { adjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:dimensions cropRect:CGRectMake(0.0f, 0.0f, dimensions.width, dimensions.height) cropOrientation:UIImageOrientationUp cropRotation:0.0 cropLockedAspectRatio:1.0 cropMirrored:false trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:nil paintingData:nil sendAsGif:false preset:TGMediaVideoConversionPresetVideoMessage]; duration = trimEndValue - trimStartValue; } if (trimStartValue > DBL_EPSILON) { bool generatedImage = false; AVAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil]; if (asset != nil) { AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; imageGenerator.maximumSize = dimensions; imageGenerator.appliesPreferredTrackTransform = true; CGImageRef imageRef = [imageGenerator copyCGImageAtTime:CMTimeMakeWithSeconds(trimStartValue, 24) actualTime:nil error:nil]; if (imageRef != nil) { image = [UIImage imageWithCGImage:imageRef]; CGImageRelease(imageRef); generatedImage = true; } } if (!generatedImage) { NSArray *thumbnail = [self thumbnailsForTimestamps:@[@(trimStartValue)]]; image = thumbnail.firstObject; } } } if (!_dismissed) { self.finishedWithVideo(url, image, fileSize, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp); } else { [[NSFileManager defaultManager] removeItemAtURL:url error:NULL]; if (self.finishedWithVideo != nil) { self.finishedWithVideo(nil, nil, 0, 0.0, CGSizeZero, nil, nil, false, 0); } } } - (UIImageOrientation)orientationForThumbnailWithTransform:(CGAffineTransform)transform mirrored:(bool)mirrored { CGFloat angle = atan2(transform.b, transform.a); NSInteger degrees = (360 + (NSInteger)TGRadiansToDegrees(angle)) % 360; switch (degrees) { case 90: return mirrored ? UIImageOrientationLeft : UIImageOrientationRight; break; case 180: return UIImageOrientationDown; break; case 270: return mirrored ? UIImageOrientationLeft : UIImageOrientationRight; default: break; } return UIImageOrientationUp; } #pragma mark - - (void)startRecordingTimer { [_controlsView recordingStarted]; [_controlsView setDurationString:@"0:00,00"]; self.onDuration(0); _audioRecordingDurationSeconds = 0; _audioRecordingDurationMilliseconds = 0.0; _recordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(timerEvent) interval:2.0 / 60.0 repeat:false]; } - (void)timerEvent { if (_recordingTimer != nil) { [_recordingTimer invalidate]; _recordingTimer = nil; } NSTimeInterval recordingDuration = _capturePipeline.videoDuration; if (isnan(recordingDuration)) recordingDuration = 0.0; if (recordingDuration < _previousDuration) recordingDuration = _previousDuration; _previousDuration = recordingDuration; [_ringView setValue:recordingDuration / TGVideoMessageMaximumDuration]; CFAbsoluteTime currentTime = CACurrentMediaTime(); NSUInteger currentDurationSeconds = (NSUInteger)recordingDuration; NSUInteger currentDurationMilliseconds = (int)(recordingDuration * 100.0f) % 100; if (currentDurationSeconds == _audioRecordingDurationSeconds && currentDurationMilliseconds == _audioRecordingDurationMilliseconds) { _recordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(timerEvent) interval:MAX(0.01, _audioRecordingDurationSeconds + 2.0 / 60.0 - currentTime) repeat:false]; } else { self.onDuration(recordingDuration); _audioRecordingDurationSeconds = currentDurationSeconds; _audioRecordingDurationMilliseconds = currentDurationMilliseconds; [_controlsView setDurationString:[[NSString alloc] initWithFormat:@"%d:%02d,%02d", (int)_audioRecordingDurationSeconds / 60, (int)_audioRecordingDurationSeconds % 60, (int)_audioRecordingDurationMilliseconds]]; _recordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(timerEvent) interval:2.0 / 60.0 repeat:false]; } if (recordingDuration >= TGVideoMessageMaximumDuration) { [_recordingTimer invalidate]; _recordingTimer = nil; _automaticDismiss = true; [self stop]; if (self.onStop != nil) self.onStop(); } } - (void)stopRecordingTimer { if (_recordingTimer != nil) { [_recordingTimer invalidate]; _recordingTimer = nil; } } #pragma mark - - (void)captureStarted { bool firstTime = !_alreadyStarted; _alreadyStarted = true; _switchButton.frame = CGRectMake(11.0f, _controlsFrame.origin.y - _switchButton.frame.size.height - 7.0f, _switchButton.frame.size.width, _switchButton.frame.size.height); NSTimeInterval delay = firstTime ? 0.1 : 0.2; [UIView animateWithDuration:0.3 delay:delay options:kNilOptions animations:^ { _placeholderView.alpha = 0.0f; _shimmerView.alpha = 0.0f; _switchButton.alpha = 1.0f; } completion:^(__unused BOOL finished) { _shimmerView.hidden = true; _placeholderView.hidden = true; _placeholderView.alpha = 1.0f; }]; if (firstTime) { TGDispatchAfter(0.2, dispatch_get_main_queue(), ^ { [self startRecording]; }); } } - (void)stopCapture { [_capturePipeline stopRunning]; } - (void)configureCamera { _capturePipeline = [[TGVideoCameraPipeline alloc] initWithDelegate:self position:_preferredPosition callbackQueue:dispatch_get_main_queue() liveUploadInterface:_liveUploadInterface]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" _capturePipeline.orientation = (AVCaptureVideoOrientation)self.interfaceOrientation; #pragma clang diagnostic pop __weak TGVideoMessageCaptureController *weakSelf = self; _capturePipeline.micLevel = ^(CGFloat level) { TGDispatchOnMainThread(^ { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil && strongSelf.micLevel != nil) strongSelf.micLevel(level); }); }; } #pragma mark - - (void)capturePipeline:(TGVideoCameraPipeline *)__unused capturePipeline didStopRunningWithError:(NSError *)__unused error { } - (void)capturePipeline:(TGVideoCameraPipeline *)__unused capturePipeline previewPixelBufferReadyForDisplay:(CVPixelBufferRef)previewPixelBuffer { if (!_gpuAvailable) return; if (!_previewView) [self setupPreviewView]; [_previewView displayPixelBuffer:previewPixelBuffer]; } - (void)capturePipelineDidRunOutOfPreviewBuffers:(TGVideoCameraPipeline *)__unused capturePipeline { if (_gpuAvailable) [_previewView flushPixelBufferCache]; } - (void)capturePipelineRecordingDidStart:(TGVideoCameraPipeline *)__unused capturePipeline { __weak TGVideoMessageCaptureController *weakSelf = self; [_activityDisposable setDisposable:[[[SSignal complete] delay:0.3 onQueue:[SQueue mainQueue]] startStrictWithNext:nil error:nil completed:^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil && strongSelf->_requestActivityHolder) { strongSelf->_activityHolder = strongSelf->_requestActivityHolder(); } } file:__FILE_NAME__ line:__LINE__]]; } - (void)capturePipelineRecordingWillStop:(TGVideoCameraPipeline *)__unused capturePipeline { } - (void)capturePipelineRecordingDidStop:(TGVideoCameraPipeline *)__unused capturePipeline duration:(NSTimeInterval)duration liveUploadData:(id)liveUploadData thumbnailImage:(UIImage *)thumbnailImage thumbnails:(NSDictionary *)thumbnails { if (_stopped && duration > 0.33) { _duration = duration; _liveUploadData = liveUploadData; _thumbnailImage = thumbnailImage; _thumbnails = thumbnails; TGDispatchOnMainThread(^ { [self _stop]; [self setupVideoView]; }); } else { [self finishWithURL:_url dimensions:CGSizeMake(240.0f, 240.0f) duration:duration liveUploadData:liveUploadData thumbnailImage:thumbnailImage isSilent:false scheduleTimestamp:0]; } } - (void)capturePipeline:(TGVideoCameraPipeline *)__unused capturePipeline recordingDidFailWithError:(NSError *)__unused error { } #pragma mark - - (void)_beginAudioSession:(bool)speaker { [_queue dispatch:^ { _otherAudioPlaying = [[AVAudioSession sharedInstance] isOtherAudioPlaying]; __weak TGVideoMessageCaptureController *weakSelf = self; id disposable = [[LegacyComponentsGlobals provider] requestAudioSession:speaker ? TGAudioSessionTypePlayAndRecordHeadphones : TGAudioSessionTypePlayAndRecord activated:^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_queue dispatch:^ { [strongSelf->_capturePipeline startRunning]; }]; } } interrupted:^ { TGDispatchOnMainThread(^{ __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_automaticDismiss = true; [strongSelf complete]; } }); }]; [_currentAudioSession setDisposable:disposable]; }]; } - (void)_endAudioSession { id currentAudioSession = _currentAudioSession; [_queue dispatch:^ { [currentAudioSession dispose]; }]; } #pragma mark - static UIImage *startImage = nil; + (NSString *)_startImagePath { return [[[LegacyComponentsGlobals provider] dataCachePath] stringByAppendingPathComponent:@"startImage.jpg"]; } + (UIImage *)startImage { if (startImage == nil) startImage = [UIImage imageWithContentsOfFile:[self _startImagePath]] ? : TGComponentsImageNamed (@"VideoMessagePlaceholder.jpg"); return startImage; } + (void)saveStartImage:(UIImage *)image { if (image == nil) return; [self clearStartImage]; startImage = image; NSData *data = UIImageJPEGRepresentation(image, 0.8f); [data writeToFile:[self _startImagePath] atomically:true]; } + (void)clearStartImage { startImage = nil; [[NSFileManager defaultManager] removeItemAtPath:[self _startImagePath] error:NULL]; } + (void)requestCameraAccess:(void (^)(bool granted, bool wasNotDetermined))resultBlock { if (iosMajorVersion() < 7) { if (resultBlock != nil) resultBlock(true, false); return; } bool wasNotDetermined = ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusNotDetermined); [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { TGDispatchOnMainThread(^ { if (resultBlock != nil) resultBlock(granted, wasNotDetermined); }); }]; } + (void)requestMicrophoneAccess:(void (^)(bool granted, bool wasNotDetermined))resultBlock { if (iosMajorVersion() < 7) { if (resultBlock != nil) resultBlock(true, false); return; } bool wasNotDetermined = ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio] == AVAuthorizationStatusNotDetermined); [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) { TGDispatchOnMainThread(^ { if (resultBlock != nil) resultBlock(granted, wasNotDetermined); }); }]; } #pragma mark - Scrubbing - (NSTimeInterval)videoScrubberDuration:(TGVideoMessageScrubber *)__unused videoScrubber { return _duration; } - (void)videoScrubberDidBeginScrubbing:(TGVideoMessageScrubber *)__unused videoScrubber { } - (void)videoScrubberDidEndScrubbing:(TGVideoMessageScrubber *)__unused videoScrubber { } - (void)videoScrubber:(TGVideoMessageScrubber *)__unused videoScrubber valueDidChange:(NSTimeInterval)__unused position { } #pragma mark - Trimming - (void)videoScrubberDidBeginEditing:(TGVideoMessageScrubber *)__unused videoScrubber { [_player pause]; } - (void)videoScrubberDidEndEditing:(TGVideoMessageScrubber *)videoScrubber endValueChanged:(bool)endValueChanged { [self updatePlayerRange:videoScrubber.trimEndValue]; if (endValueChanged) [self _seekToPosition:videoScrubber.trimStartValue]; [_player play]; } - (void)videoScrubber:(TGVideoMessageScrubber *)__unused videoScrubber editingStartValueDidChange:(NSTimeInterval)startValue { [self _seekToPosition:startValue]; } - (void)videoScrubber:(TGVideoMessageScrubber *)__unused videoScrubber editingEndValueDidChange:(NSTimeInterval)endValue { [self _seekToPosition:endValue]; } - (void)updatePlayerRange:(NSTimeInterval)trimEndValue { _player.currentItem.forwardPlaybackEndTime = CMTimeMakeWithSeconds(trimEndValue, NSEC_PER_SEC); } #pragma mark - Thumbnails - (CGFloat)videoScrubberThumbnailAspectRatio:(TGVideoMessageScrubber *)__unused videoScrubber { return 1.0f; } - (NSArray *)videoScrubber:(TGVideoMessageScrubber *)videoScrubber evenlySpacedTimestamps:(NSInteger)count startingAt:(NSTimeInterval)startTimestamp endingAt:(NSTimeInterval)endTimestamp { if (endTimestamp < startTimestamp) return nil; if (count == 0) return nil; NSTimeInterval duration = [self videoScrubberDuration:videoScrubber]; if (endTimestamp > duration) endTimestamp = duration; NSTimeInterval interval = (endTimestamp - startTimestamp) / count; NSMutableArray *timestamps = [[NSMutableArray alloc] init]; for (NSInteger i = 0; i < count; i++) [timestamps addObject:@(startTimestamp + i * interval)]; return timestamps; } - (NSArray *)thumbnailsForTimestamps:(NSArray *)timestamps { NSArray *thumbnailTimestamps = [_thumbnails.allKeys sortedArrayUsingSelector:@selector(compare:)]; NSMutableArray *thumbnails = [[NSMutableArray alloc] init]; __block NSUInteger i = 1; [timestamps enumerateObjectsUsingBlock:^(NSNumber *timestampVal, __unused NSUInteger index, __unused BOOL *stop) { NSTimeInterval timestamp = timestampVal.doubleValue; NSNumber *closestTimestamp = [self closestTimestampForTimestamp:timestamp timestamps:thumbnailTimestamps start:i finalIndex:&i]; if (closestTimestamp != nil) { [thumbnails addObject:_thumbnails[closestTimestamp]]; } }]; return thumbnails; } - (NSNumber *)closestTimestampForTimestamp:(NSTimeInterval)timestamp timestamps:(NSArray *)timestamps start:(NSUInteger)start finalIndex:(NSUInteger *)finalIndex { if (start >= timestamps.count) { return nil; } NSTimeInterval leftTimestamp = [timestamps[start - 1] doubleValue]; NSTimeInterval rightTimestamp = [timestamps[start] doubleValue]; if (fabs(leftTimestamp - timestamp) < fabs(rightTimestamp - timestamp)) { *finalIndex = start; return timestamps[start - 1]; } else { if (start == timestamps.count - 1) { *finalIndex = start; return timestamps[start]; } return [self closestTimestampForTimestamp:timestamp timestamps:timestamps start:start + 1 finalIndex:finalIndex]; } } - (void)videoScrubber:(TGVideoMessageScrubber *)__unused videoScrubber requestThumbnailImagesForTimestamps:(NSArray *)timestamps size:(CGSize)__unused size isSummaryThumbnails:(bool)isSummaryThumbnails { if (timestamps.count == 0) return; NSArray *thumbnails = [self thumbnailsForTimestamps:timestamps]; [thumbnails enumerateObjectsUsingBlock:^(UIImage *image, NSUInteger index, __unused BOOL *stop) { if (index < timestamps.count) [_controlsView.scrubberView setThumbnailImage:image forTimestamp:[timestamps[index] doubleValue] isSummaryThubmnail:isSummaryThumbnails]; }]; } - (void)videoScrubberDidFinishRequestingThumbnails:(TGVideoMessageScrubber *)__unused videoScrubber { [_controlsView showScrubberView]; } - (void)videoScrubberDidCancelRequestingThumbnails:(TGVideoMessageScrubber *)__unused videoScrubber { } - (CGSize)videoScrubberOriginalSize:(TGVideoMessageScrubber *)__unused videoScrubber cropRect:(CGRect *)cropRect cropOrientation:(UIImageOrientation *)cropOrientation cropMirrored:(bool *)cropMirrored { if (cropRect != NULL) *cropRect = CGRectMake(0.0f, 0.0f, 240.0f, 240.0f); if (cropOrientation != NULL) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) *cropOrientation = UIImageOrientationUp; else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft) *cropOrientation = UIImageOrientationRight; else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight) *cropOrientation = UIImageOrientationLeft; #pragma clang diagnostic pop } if (cropMirrored != NULL) *cropMirrored = false; return CGSizeMake(240.0f, 240.0f); } - (UIView *)extractVideoContent { UIView *result = [_circleView snapshotViewAfterScreenUpdates:false]; result.frame = [_circleView convertRect:_circleView.bounds toView:nil]; return result; } - (void)hideVideoContent { _circleWrapperView.alpha = 0.02f; } @end @implementation TGVideoMessageCaptureControllerWindow - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { bool flag = [super pointInside:point withEvent:event]; if (_locked) { if (point.x >= self.frame.size.width - 60.0f && point.y >= self.controlsFrame.origin.y && point.y < CGRectGetMaxY(self.controlsFrame)) return false; } return flag; } @end