Kateryna Kostiuk | fbe1b2f | 2019-10-07 17:32:26 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 Savoir-faire Linux Inc. |
| 3 | * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> |
| 4 | * |
| 5 | * This program is free software; you can redistribute it and/or modify |
| 6 | * it under the terms of the GNU General Public License as published by |
| 7 | * the Free Software Foundation; either version 3 of the License, or |
| 8 | * (at your option) any later version. |
| 9 | * |
| 10 | * This program is distributed in the hope that it will be useful, |
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | * GNU General Public License for more details. |
| 14 | * |
| 15 | * You should have received a copy of the GNU General Public License |
| 16 | * along with this program; if not, write to the Free Software |
| 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| 18 | */ |
| 19 | |
| 20 | #import "RecordFileVC.h" |
| 21 | #import "AppDelegate.h" |
| 22 | #import "VideoCommon.h" |
| 23 | #import "views/HoverButton.h" |
| 24 | #import "views/CallMTKView.h" |
| 25 | #import "views/NSColor+RingTheme.h" |
| 26 | #import "NSString+Extensions.h" |
| 27 | |
| 28 | //lrc |
| 29 | #import <video/renderer.h> |
| 30 | #import <api/avmodel.h> |
| 31 | |
| 32 | #import <AVFoundation/AVFoundation.h> |
| 33 | |
| 34 | @interface RecordFileVC () |
| 35 | @property (unsafe_unretained) IBOutlet CallMTKView* previewView; |
| 36 | |
| 37 | @property (unsafe_unretained) IBOutlet NSTextField* timeLabel; |
| 38 | @property (unsafe_unretained) IBOutlet NSTextField* infoLabel; |
| 39 | |
| 40 | @property (unsafe_unretained) IBOutlet HoverButton* recordOnOffButton; |
Kateryna Kostiuk | fbe1b2f | 2019-10-07 17:32:26 -0400 | [diff] [blame] | 41 | @property (unsafe_unretained) IBOutlet NSButton *sendButton; |
| 42 | @property (unsafe_unretained) IBOutlet HoverButton *fileImage; |
| 43 | |
| 44 | @property (assign) IBOutlet NSLayoutConstraint* timeRightConstraint; |
| 45 | @property (assign) IBOutlet NSLayoutConstraint* timeTopConstraint; |
| 46 | @property (assign) IBOutlet NSLayoutConstraint* timeCenterX; |
| 47 | @property (assign) IBOutlet NSLayoutConstraint* timeCenterY; |
| 48 | |
| 49 | @property RendererConnectionsHolder* renderConnections; |
| 50 | |
| 51 | @end |
| 52 | |
| 53 | @implementation RecordFileVC |
| 54 | |
| 55 | CVPixelBufferPoolRef pool; |
| 56 | CVPixelBufferRef pixBuf; |
| 57 | BOOL recording; |
| 58 | NSString *fileName; |
| 59 | NSTimer* durationTimer; |
| 60 | int timePassing = 0; |
| 61 | bool isAudio = NO; |
| 62 | |
| 63 | @synthesize avModel, renderConnections, |
| 64 | previewView, timeLabel, recordOnOffButton, sendButton, fileImage, infoLabel, timeRightConstraint,timeTopConstraint, timeCenterX, timeCenterY; |
| 65 | |
| 66 | -(id) initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil avModel:(lrc::api::AVModel*) avModel |
| 67 | { |
| 68 | if (self = [self initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) |
| 69 | { |
| 70 | self.avModel = avModel; |
| 71 | renderConnections = [[RendererConnectionsHolder alloc] init]; |
| 72 | } |
| 73 | return self; |
| 74 | } |
| 75 | |
| 76 | - (void)loadView { |
| 77 | [super loadView]; |
| 78 | [[self view] setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; |
| 79 | [self.previewView setupView]; |
| 80 | AppDelegate* appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate]; |
| 81 | if ([appDelegate getActiveCalls].size()) { |
| 82 | [self setErrorState]; |
| 83 | return; |
| 84 | } |
| 85 | [self setInitialState]; |
| 86 | } |
| 87 | |
| 88 | - (void) connectPreviewSignals { |
| 89 | [previewView fillWithBlack]; |
| 90 | QObject::disconnect(renderConnections.frameUpdated); |
| 91 | renderConnections.frameUpdated = |
| 92 | QObject::connect(avModel, |
| 93 | &lrc::api::AVModel::frameUpdated, |
| 94 | [=](const std::string& id) { |
| 95 | if (id != lrc::api::video::PREVIEW_RENDERER_ID) { |
| 96 | return; |
| 97 | } |
| 98 | auto renderer = &avModel->getRenderer(id); |
| 99 | if(!renderer->isRendering()) { |
| 100 | return; |
| 101 | } |
| 102 | [self renderer:renderer |
| 103 | renderFrameForView: self.previewView]; |
| 104 | }); |
| 105 | } |
| 106 | |
| 107 | #pragma mark - dispaly |
| 108 | |
| 109 | -(void) renderer: (const lrc::api::video::Renderer*)renderer renderFrameForView:(CallMTKView*) view |
| 110 | { |
| 111 | @autoreleasepool { |
| 112 | const CGSize frameSize = [VideoCommon fillPixelBuffr:&pixBuf |
| 113 | fromRenderer:renderer |
| 114 | bufferPool:&pool]; |
| 115 | if(frameSize.width == 0 || frameSize.height == 0) { |
| 116 | return; |
| 117 | } |
| 118 | CVPixelBufferRef buffer = pixBuf; |
| 119 | [view renderWithPixelBuffer: buffer |
| 120 | size: frameSize |
| 121 | rotation: 0 |
| 122 | fillFrame: false]; |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | #pragma mark - actions |
| 127 | |
| 128 | - (IBAction)cancell:(NSButton *)sender { |
| 129 | [self disconnectVideo]; |
| 130 | self.delegate.closeRecordingView; |
| 131 | } |
| 132 | |
| 133 | - (IBAction)sendMessage:(NSButton *)sender { |
| 134 | NSArray* pathURL = [fileName componentsSeparatedByString: @"/"]; |
| 135 | if([pathURL count] < 1) { |
| 136 | return; |
| 137 | } |
| 138 | NSString* name = [pathURL objectAtIndex: [pathURL count] - 1]; |
| 139 | [self.delegate sendFile:name withFilePath:fileName]; |
| 140 | self.delegate.closeRecordingView; |
| 141 | } |
| 142 | |
| 143 | - (IBAction)togleRecord:(NSButton *)sender { |
| 144 | if (recording) { |
| 145 | [self stopRecord]; |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | NSString *info = NSLocalizedString(@"Press to start recording", @"Recording view explanation label"); |
| 150 | infoLabel.stringValue = info; |
| 151 | |
| 152 | #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 |
| 153 | if (@available(macOS 10.14, *)) { |
| 154 | NSString *noVideoPermission = NSLocalizedString(@"Video permission not granted", @"Error video permission"); |
| 155 | NSString *noAudioPermission = NSLocalizedString(@"Audio permission not granted", @"Error audio permission"); |
| 156 | |
| 157 | AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; |
| 158 | if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied) |
| 159 | { |
| 160 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 161 | infoLabel.stringValue = noAudioPermission; |
| 162 | }); |
| 163 | return; |
| 164 | } |
| 165 | |
| 166 | if(authStatus == AVAuthorizationStatusNotDetermined) |
| 167 | { |
| 168 | [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { |
| 169 | if(!granted){ |
| 170 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 171 | infoLabel.stringValue = noAudioPermission; |
| 172 | }); |
| 173 | return; |
| 174 | } |
| 175 | [self startRecord]; |
| 176 | }]; |
| 177 | return; |
| 178 | } |
| 179 | if (!isAudio) { |
| 180 | AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; |
| 181 | if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied) |
| 182 | { |
| 183 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 184 | infoLabel.stringValue = noVideoPermission; |
| 185 | }); |
| 186 | return; |
| 187 | } |
| 188 | |
| 189 | if(authStatus == AVAuthorizationStatusNotDetermined) |
| 190 | { |
| 191 | [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { |
| 192 | if(!granted){ |
| 193 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 194 | infoLabel.stringValue = noVideoPermission; |
| 195 | }); |
| 196 | return; |
| 197 | } |
| 198 | [self startRecord]; |
| 199 | }]; |
| 200 | return; |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | #endif |
| 205 | [self startRecord]; |
| 206 | } |
| 207 | |
| 208 | -(void) stopRecord { |
| 209 | avModel->stopLocalRecorder([fileName UTF8String]); |
| 210 | recording = false; |
| 211 | [durationTimer invalidate]; |
| 212 | durationTimer = nil; |
| 213 | [self setRecordedState]; |
| 214 | if(isAudio) { |
| 215 | return; |
| 216 | } |
| 217 | std::string uri = [[@"file:///" stringByAppendingString: fileName] UTF8String]; |
| 218 | avModel->setInputFile(uri); |
| 219 | } |
| 220 | |
| 221 | -(void) startRecord { |
| 222 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 223 | if (!isAudio) { |
| 224 | avModel->startPreview(); |
| 225 | } |
| 226 | [self setRecordingState]; |
| 227 | std::string file_name = avModel->startLocalRecorder(isAudio); |
| 228 | if (file_name.empty()) { |
| 229 | return; |
| 230 | } |
| 231 | fileName = @(file_name.c_str()); |
| 232 | recording = true; |
| 233 | if (durationTimer == nil) |
| 234 | durationTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 |
| 235 | target:self |
| 236 | selector:@selector(updateDurationLabel) |
| 237 | userInfo:nil |
| 238 | repeats:YES]; |
| 239 | }); |
| 240 | } |
| 241 | |
| 242 | -(void) updateDurationLabel |
| 243 | { |
| 244 | timePassing++; |
| 245 | [timeLabel setStringValue: [NSString formattedStringTimeFromSeconds: timePassing]]; |
| 246 | } |
| 247 | |
| 248 | -(void) stopRecordingView { |
| 249 | [self disconnectVideo]; |
| 250 | recording = false; |
| 251 | [durationTimer invalidate]; |
| 252 | durationTimer = nil; |
| 253 | [recordOnOffButton stopBlinkAnimation]; |
Kateryna Kostiuk | fbe1b2f | 2019-10-07 17:32:26 -0400 | [diff] [blame] | 254 | } |
| 255 | |
| 256 | -(void) disconnectVideo { |
| 257 | AppDelegate* appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate]; |
| 258 | if (![appDelegate getActiveCalls].size()) { |
| 259 | avModel->stopPreview(); |
| 260 | QObject::disconnect(renderConnections.frameUpdated); |
| 261 | avModel->stopLocalRecorder([fileName UTF8String]); |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | -(void) prepareRecordingView:(BOOL)audioOnly { |
| 266 | AppDelegate* appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate]; |
| 267 | if ([appDelegate getActiveCalls].size()) { |
| 268 | [self setErrorState]; |
| 269 | return; |
| 270 | } |
| 271 | isAudio = audioOnly; |
| 272 | [self setInitialState]; |
| 273 | if (isAudio) { |
| 274 | return; |
| 275 | } |
| 276 | [previewView fillWithBlack]; |
| 277 | |
| 278 | self.previewView.stopRendering = false; |
| 279 | [self connectPreviewSignals]; |
| 280 | avModel->stopPreview(); |
| 281 | avModel->startPreview(); |
| 282 | } |
| 283 | |
| 284 | -(void) setInitialState { |
| 285 | [recordOnOffButton setHidden:NO]; |
| 286 | [infoLabel setHidden:NO]; |
| 287 | [sendButton setHidden:YES]; |
| 288 | [sendButton setHidden:YES]; |
| 289 | [fileImage setHidden:YES]; |
| 290 | [timeLabel setStringValue: @""]; |
| 291 | |
| 292 | fileName = @""; |
| 293 | timePassing = 0; |
| 294 | |
| 295 | NSColor *color = isAudio ? [NSColor labelColor] : [NSColor whiteColor]; |
| 296 | recordOnOffButton.moiuseOutsideImageColor = color; |
| 297 | recordOnOffButton.imageColor = color; |
| 298 | fileImage.buttonDisableColor = color; |
| 299 | fileImage.imageColor = color; |
| 300 | timeLabel.textColor = color; |
| 301 | infoLabel.textColor = color; |
| 302 | NSString *title = NSLocalizedString(@"Send", @"Send button title"); |
Kateryna Kostiuk | a740481 | 2019-10-28 12:24:46 -0400 | [diff] [blame^] | 303 | NSString *info = NSLocalizedString(@"Press to start recording", @"Recording view explanation label"); |
| 304 | infoLabel.stringValue = info; |
Kateryna Kostiuk | fbe1b2f | 2019-10-07 17:32:26 -0400 | [diff] [blame] | 305 | NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; |
| 306 | [style setAlignment:NSCenterTextAlignment]; |
| 307 | NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:color, NSForegroundColorAttributeName, style, NSParagraphStyleAttributeName, nil]; |
| 308 | NSAttributedString *attrString = [[NSAttributedString alloc]initWithString:title attributes:attrsDictionary]; |
| 309 | [sendButton setAttributedTitle:attrString]; |
| 310 | |
| 311 | [previewView setHidden:isAudio]; |
| 312 | auto frame = self.view.frame; |
| 313 | if (isAudio) { |
| 314 | [self.view setFrameSize: CGSizeMake(370, 160)]; |
| 315 | timeRightConstraint.priority = 200; |
| 316 | timeTopConstraint.priority = 200; |
| 317 | timeCenterX.priority = 900; |
| 318 | timeCenterY.priority = 900; |
| 319 | timeCenterX.constant = 0; |
| 320 | return; |
| 321 | } |
| 322 | timeRightConstraint.priority = 900; |
| 323 | timeTopConstraint.priority = 900; |
| 324 | timeCenterX.priority = 200; |
| 325 | timeCenterY.priority = 200; |
| 326 | [self.view setFrameSize: CGSizeMake(480, 270)]; |
| 327 | previewView.frame = self.view.bounds; |
| 328 | } |
| 329 | |
| 330 | -(void) setRecordingState { |
| 331 | fileName = @""; |
| 332 | timePassing = 0; |
| 333 | [recordOnOffButton setHidden:NO]; |
| 334 | [sendButton setHidden:YES]; |
| 335 | [fileImage setHidden:YES]; |
| 336 | [infoLabel setHidden:YES]; |
| 337 | [timeLabel setStringValue: @""]; |
| 338 | NSString *info = NSLocalizedString(@"Press to start recording", @"Recording view explanation label"); |
| 339 | infoLabel.stringValue = info; |
| 340 | [recordOnOffButton startBlinkAnimationfrom:[NSColor buttonBlinkColorColor] |
| 341 | to:[NSColor whiteColor] |
| 342 | scaleFactor: 1 |
| 343 | duration: 1.5]; |
| 344 | timeCenterX.constant = 0; |
| 345 | } |
| 346 | |
| 347 | -(void) setRecordedState { |
| 348 | [recordOnOffButton stopBlinkAnimation]; |
| 349 | [recordOnOffButton setHidden:NO]; |
| 350 | [sendButton setHidden:NO]; |
| 351 | [fileImage setHidden:NO]; |
| 352 | timeCenterX.constant = 15; |
| 353 | [infoLabel setHidden:YES]; |
| 354 | } |
| 355 | |
| 356 | //when open during call |
| 357 | -(void) setErrorState { |
| 358 | NSString *info = NSLocalizedString(@"Could not record message during call", @"Recording view explanation label"); |
| 359 | infoLabel.stringValue = info; |
| 360 | [infoLabel setHidden:NO]; |
| 361 | [recordOnOffButton setHidden:YES]; |
| 362 | infoLabel.textColor = [NSColor textColor]; |
| 363 | [previewView setHidden:YES]; |
Kateryna Kostiuk | a740481 | 2019-10-28 12:24:46 -0400 | [diff] [blame^] | 364 | [sendButton setHidden:YES]; |
| 365 | [fileImage setHidden:YES]; |
| 366 | [timeLabel setStringValue: @""]; |
Kateryna Kostiuk | fbe1b2f | 2019-10-07 17:32:26 -0400 | [diff] [blame] | 367 | [self.view setFrameSize: CGSizeMake(370, 160)]; |
| 368 | } |
| 369 | |
| 370 | @end |