blob: 013f5cc0af705ba89261d706464ccef14f00d234 [file] [log] [blame]
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001/*
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 Kostiukfbe1b2f2019-10-07 17:32:26 -040041@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
55CVPixelBufferPoolRef pool;
56CVPixelBufferRef pixBuf;
57BOOL recording;
58NSString *fileName;
59NSTimer* durationTimer;
60int timePassing = 0;
61bool isAudio = NO;
62
63@synthesize avModel, renderConnections,
64previewView, 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 Kostiukfbe1b2f2019-10-07 17:32:26 -0400254}
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 Kostiuka7404812019-10-28 12:24:46 -0400303 NSString *info = NSLocalizedString(@"Press to start recording", @"Recording view explanation label");
304 infoLabel.stringValue = info;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400305 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 Kostiuka7404812019-10-28 12:24:46 -0400364 [sendButton setHidden:YES];
365 [fileImage setHidden:YES];
366 [timeLabel setStringValue: @""];
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400367 [self.view setFrameSize: CGSizeMake(370, 160)];
368}
369
370@end