1/*
2 *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#import "APPRTCViewController.h"
12
13#import <AVFoundation/AVFoundation.h>
14
15#import <WebRTC/RTCMTLNSVideoView.h>
16#import <WebRTC/RTCNSGLVideoView.h>
17#import <WebRTC/RTCVideoTrack.h>
18
19#import "ARDAppClient.h"
20#import "ARDCaptureController.h"
21#import "ARDSettingsModel.h"
22
23static NSUInteger const kContentWidth = 900;
24static NSUInteger const kRoomFieldWidth = 200;
25static NSUInteger const kActionItemHeight = 30;
26static NSUInteger const kBottomViewHeight = 200;
27
28@class APPRTCMainView;
29@protocol APPRTCMainViewDelegate
30
31- (void)appRTCMainView:(APPRTCMainView*)mainView
32        didEnterRoomId:(NSString*)roomId
33              loopback:(BOOL)isLoopback;
34
35@end
36
37@interface APPRTCMainView : NSView
38
39@property(nonatomic, weak) id<APPRTCMainViewDelegate> delegate;
40@property(nonatomic, readonly) NSView<RTC_OBJC_TYPE(RTCVideoRenderer)>* localVideoView;
41@property(nonatomic, readonly) NSView<RTC_OBJC_TYPE(RTCVideoRenderer)>* remoteVideoView;
42@property(nonatomic, readonly) NSTextView* logView;
43
44- (void)displayLogMessage:(NSString*)message;
45
46@end
47
48@interface APPRTCMainView () <NSTextFieldDelegate, RTC_OBJC_TYPE (RTCNSGLVideoViewDelegate)>
49@end
50@implementation APPRTCMainView  {
51  NSScrollView* _scrollView;
52  NSView* _actionItemsView;
53  NSButton* _connectButton;
54  NSButton* _loopbackButton;
55  NSTextField* _roomField;
56  CGSize _localVideoSize;
57  CGSize _remoteVideoSize;
58}
59
60@synthesize delegate = _delegate;
61@synthesize localVideoView = _localVideoView;
62@synthesize remoteVideoView = _remoteVideoView;
63@synthesize logView = _logView;
64
65- (void)displayLogMessage:(NSString *)message {
66  dispatch_async(dispatch_get_main_queue(), ^{
67    self.logView.string = [NSString stringWithFormat:@"%@%@\n", self.logView.string, message];
68    NSRange range = NSMakeRange(self.logView.string.length, 0);
69    [self.logView scrollRangeToVisible:range];
70  });
71}
72
73#pragma mark - Private
74
75- (instancetype)initWithFrame:(NSRect)frame {
76  if (self = [super initWithFrame:frame]) {
77    [self setupViews];
78  }
79  return self;
80}
81
82+ (BOOL)requiresConstraintBasedLayout {
83  return YES;
84}
85
86- (void)updateConstraints {
87  NSParameterAssert(
88      _roomField != nil &&
89      _scrollView != nil &&
90      _remoteVideoView != nil &&
91      _localVideoView != nil &&
92      _actionItemsView!= nil &&
93      _connectButton != nil &&
94      _loopbackButton != nil);
95
96  [self removeConstraints:[self constraints]];
97  NSDictionary* viewsDictionary =
98      NSDictionaryOfVariableBindings(_roomField,
99                                     _scrollView,
100                                     _remoteVideoView,
101                                     _localVideoView,
102                                     _actionItemsView,
103                                     _connectButton,
104                                     _loopbackButton);
105
106  NSSize remoteViewSize = [self remoteVideoViewSize];
107  NSDictionary* metrics = @{
108    @"remoteViewWidth" : @(remoteViewSize.width),
109    @"remoteViewHeight" : @(remoteViewSize.height),
110    @"kBottomViewHeight" : @(kBottomViewHeight),
111    @"localViewHeight" : @(remoteViewSize.height / 3),
112    @"localViewWidth" : @(remoteViewSize.width / 3),
113    @"kRoomFieldWidth" : @(kRoomFieldWidth),
114    @"kActionItemHeight" : @(kActionItemHeight)
115  };
116  // Declare this separately to avoid compiler warning about splitting string
117  // within an NSArray expression.
118  NSString* verticalConstraintLeft =
119      @"V:|-[_remoteVideoView(remoteViewHeight)]-[_scrollView(kBottomViewHeight)]-|";
120  NSString* verticalConstraintRight =
121      @"V:|-[_remoteVideoView(remoteViewHeight)]-[_actionItemsView(kBottomViewHeight)]-|";
122  NSArray* constraintFormats = @[
123      verticalConstraintLeft,
124      verticalConstraintRight,
125      @"H:|-[_remoteVideoView(remoteViewWidth)]-|",
126      @"V:|-[_localVideoView(localViewHeight)]",
127      @"H:|-[_localVideoView(localViewWidth)]",
128      @"H:|-[_scrollView(==_actionItemsView)]-[_actionItemsView]-|"
129  ];
130
131  NSArray* actionItemsConstraints = @[
132      @"H:|-[_roomField(kRoomFieldWidth)]-[_loopbackButton(kRoomFieldWidth)]",
133      @"H:|-[_connectButton(kRoomFieldWidth)]",
134      @"V:|-[_roomField(kActionItemHeight)]-[_connectButton(kActionItemHeight)]",
135      @"V:|-[_loopbackButton(kActionItemHeight)]",
136      ];
137
138  [APPRTCMainView addConstraints:constraintFormats
139                          toView:self
140                 viewsDictionary:viewsDictionary
141                         metrics:metrics];
142  [APPRTCMainView addConstraints:actionItemsConstraints
143                          toView:_actionItemsView
144                 viewsDictionary:viewsDictionary
145                         metrics:metrics];
146  [super updateConstraints];
147}
148
149#pragma mark - Constraints helper
150
151+ (void)addConstraints:(NSArray*)constraints toView:(NSView*)view
152       viewsDictionary:(NSDictionary*)viewsDictionary
153               metrics:(NSDictionary*)metrics {
154  for (NSString* constraintFormat in constraints) {
155    NSArray* constraints =
156    [NSLayoutConstraint constraintsWithVisualFormat:constraintFormat
157                                            options:0
158                                            metrics:metrics
159                                              views:viewsDictionary];
160    for (NSLayoutConstraint* constraint in constraints) {
161      [view addConstraint:constraint];
162    }
163  }
164}
165
166#pragma mark - Control actions
167
168- (void)startCall:(id)sender {
169  NSString* roomString = _roomField.stringValue;
170  // Generate room id for loopback options.
171  if (_loopbackButton.intValue && [roomString isEqualToString:@""]) {
172    roomString = [NSUUID UUID].UUIDString;
173    roomString = [roomString stringByReplacingOccurrencesOfString:@"-" withString:@""];
174  }
175  [self.delegate appRTCMainView:self
176                 didEnterRoomId:roomString
177                       loopback:_loopbackButton.intValue];
178  [self setNeedsUpdateConstraints:YES];
179}
180
181#pragma mark - RTC_OBJC_TYPE(RTCNSGLVideoViewDelegate)
182
183- (void)videoView:(RTC_OBJC_TYPE(RTCNSGLVideoView) *)videoView didChangeVideoSize:(NSSize)size {
184  if (videoView == _remoteVideoView) {
185    _remoteVideoSize = size;
186  } else if (videoView == _localVideoView) {
187    _localVideoSize = size;
188  } else {
189    return;
190  }
191
192  [self setNeedsUpdateConstraints:YES];
193}
194
195#pragma mark - Private
196
197- (void)setupViews {
198  NSParameterAssert([[self subviews] count] == 0);
199
200  _logView = [[NSTextView alloc] initWithFrame:NSZeroRect];
201  [_logView setMinSize:NSMakeSize(0, kBottomViewHeight)];
202  [_logView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
203  [_logView setVerticallyResizable:YES];
204  [_logView setAutoresizingMask:NSViewWidthSizable];
205  NSTextContainer* textContainer = [_logView textContainer];
206  NSSize containerSize = NSMakeSize(kContentWidth, FLT_MAX);
207  [textContainer setContainerSize:containerSize];
208  [textContainer setWidthTracksTextView:YES];
209  [_logView setEditable:NO];
210
211  [self setupActionItemsView];
212
213  _scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect];
214  [_scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
215  [_scrollView setHasVerticalScroller:YES];
216  [_scrollView setDocumentView:_logView];
217  [self addSubview:_scrollView];
218
219// NOTE (daniela): Ignoring Clang diagonstic here.
220// We're performing run time check to make sure class is available on runtime.
221// If not we're providing sensible default.
222#pragma clang diagnostic push
223#pragma clang diagnostic ignored "-Wpartial-availability"
224  if ([RTC_OBJC_TYPE(RTCMTLNSVideoView) class] &&
225      [RTC_OBJC_TYPE(RTCMTLNSVideoView) isMetalAvailable]) {
226    _remoteVideoView = [[RTC_OBJC_TYPE(RTCMTLNSVideoView) alloc] initWithFrame:NSZeroRect];
227    _localVideoView = [[RTC_OBJC_TYPE(RTCMTLNSVideoView) alloc] initWithFrame:NSZeroRect];
228  }
229#pragma clang diagnostic pop
230  if (_remoteVideoView == nil) {
231    NSOpenGLPixelFormatAttribute attributes[] = {
232      NSOpenGLPFADoubleBuffer,
233      NSOpenGLPFADepthSize, 24,
234      NSOpenGLPFAOpenGLProfile,
235      NSOpenGLProfileVersion3_2Core,
236      0
237    };
238    NSOpenGLPixelFormat* pixelFormat =
239    [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
240
241    RTC_OBJC_TYPE(RTCNSGLVideoView)* remote =
242        [[RTC_OBJC_TYPE(RTCNSGLVideoView) alloc] initWithFrame:NSZeroRect pixelFormat:pixelFormat];
243    remote.delegate = self;
244    _remoteVideoView = remote;
245
246    RTC_OBJC_TYPE(RTCNSGLVideoView)* local =
247        [[RTC_OBJC_TYPE(RTCNSGLVideoView) alloc] initWithFrame:NSZeroRect pixelFormat:pixelFormat];
248    local.delegate = self;
249    _localVideoView = local;
250  }
251
252  [_remoteVideoView setTranslatesAutoresizingMaskIntoConstraints:NO];
253  [self addSubview:_remoteVideoView];
254  [_localVideoView setTranslatesAutoresizingMaskIntoConstraints:NO];
255  [self addSubview:_localVideoView];
256}
257
258- (void)setupActionItemsView {
259  _actionItemsView = [[NSView alloc] initWithFrame:NSZeroRect];
260  [_actionItemsView setTranslatesAutoresizingMaskIntoConstraints:NO];
261  [self addSubview:_actionItemsView];
262
263  _roomField = [[NSTextField alloc] initWithFrame:NSZeroRect];
264  [_roomField setTranslatesAutoresizingMaskIntoConstraints:NO];
265  [[_roomField cell] setPlaceholderString: @"Enter AppRTC room id"];
266  [_actionItemsView addSubview:_roomField];
267  [_roomField setEditable:YES];
268
269  _connectButton = [[NSButton alloc] initWithFrame:NSZeroRect];
270  [_connectButton setTranslatesAutoresizingMaskIntoConstraints:NO];
271  _connectButton.title = @"Start call";
272  _connectButton.bezelStyle = NSRoundedBezelStyle;
273  _connectButton.target = self;
274  _connectButton.action = @selector(startCall:);
275  [_actionItemsView addSubview:_connectButton];
276
277  _loopbackButton = [[NSButton alloc] initWithFrame:NSZeroRect];
278  [_loopbackButton setTranslatesAutoresizingMaskIntoConstraints:NO];
279  _loopbackButton.title = @"Loopback";
280  [_loopbackButton setButtonType:NSSwitchButton];
281  [_actionItemsView addSubview:_loopbackButton];
282}
283
284- (NSSize)remoteVideoViewSize {
285  if (!_remoteVideoView.bounds.size.width) {
286    return NSMakeSize(kContentWidth, 0);
287  }
288  NSInteger width = MAX(_remoteVideoView.bounds.size.width, kContentWidth);
289  NSInteger height = (width/16) * 9;
290  return NSMakeSize(width, height);
291}
292
293@end
294
295@interface APPRTCViewController ()
296    <ARDAppClientDelegate, APPRTCMainViewDelegate>
297@property(nonatomic, readonly) APPRTCMainView* mainView;
298@end
299
300@implementation APPRTCViewController {
301  ARDAppClient* _client;
302  RTC_OBJC_TYPE(RTCVideoTrack) * _localVideoTrack;
303  RTC_OBJC_TYPE(RTCVideoTrack) * _remoteVideoTrack;
304  ARDCaptureController* _captureController;
305}
306
307- (void)dealloc {
308  [self disconnect];
309}
310
311- (void)viewDidAppear {
312  [super viewDidAppear];
313  [self displayUsageInstructions];
314}
315
316- (void)loadView {
317  APPRTCMainView* view = [[APPRTCMainView alloc] initWithFrame:NSZeroRect];
318  [view setTranslatesAutoresizingMaskIntoConstraints:NO];
319  view.delegate = self;
320  self.view = view;
321}
322
323- (void)windowWillClose:(NSNotification*)notification {
324  [self disconnect];
325}
326
327#pragma mark - Usage
328
329- (void)displayUsageInstructions {
330  [self.mainView displayLogMessage:
331   @"To start call:\n"
332   @"• Enter AppRTC room id (not neccessary for loopback)\n"
333   @"• Start call"];
334}
335
336#pragma mark - ARDAppClientDelegate
337
338- (void)appClient:(ARDAppClient *)client
339    didChangeState:(ARDAppClientState)state {
340  switch (state) {
341    case kARDAppClientStateConnected:
342      [self.mainView displayLogMessage:@"Client connected."];
343      break;
344    case kARDAppClientStateConnecting:
345      [self.mainView displayLogMessage:@"Client connecting."];
346      break;
347    case kARDAppClientStateDisconnected:
348      [self.mainView displayLogMessage:@"Client disconnected."];
349      [self resetUI];
350      _client = nil;
351      break;
352  }
353}
354
355- (void)appClient:(ARDAppClient *)client
356    didChangeConnectionState:(RTCIceConnectionState)state {
357}
358
359- (void)appClient:(ARDAppClient*)client
360    didCreateLocalCapturer:(RTC_OBJC_TYPE(RTCCameraVideoCapturer) *)localCapturer {
361  _captureController =
362      [[ARDCaptureController alloc] initWithCapturer:localCapturer
363                                            settings:[[ARDSettingsModel alloc] init]];
364  [_captureController startCapture];
365}
366
367- (void)appClient:(ARDAppClient*)client
368    didReceiveLocalVideoTrack:(RTC_OBJC_TYPE(RTCVideoTrack) *)localVideoTrack {
369  _localVideoTrack = localVideoTrack;
370  [_localVideoTrack addRenderer:self.mainView.localVideoView];
371}
372
373- (void)appClient:(ARDAppClient*)client
374    didReceiveRemoteVideoTrack:(RTC_OBJC_TYPE(RTCVideoTrack) *)remoteVideoTrack {
375  _remoteVideoTrack = remoteVideoTrack;
376  [_remoteVideoTrack addRenderer:self.mainView.remoteVideoView];
377}
378
379- (void)appClient:(ARDAppClient *)client
380         didError:(NSError *)error {
381  [self showAlertWithMessage:[NSString stringWithFormat:@"%@", error]];
382  [self disconnect];
383}
384
385- (void)appClient:(ARDAppClient *)client
386      didGetStats:(NSArray *)stats {
387}
388
389#pragma mark - APPRTCMainViewDelegate
390
391- (void)appRTCMainView:(APPRTCMainView*)mainView
392        didEnterRoomId:(NSString*)roomId
393              loopback:(BOOL)isLoopback {
394
395  if ([roomId isEqualToString:@""]) {
396    [self.mainView displayLogMessage:@"Missing room id"];
397    return;
398  }
399
400  [self disconnect];
401  ARDAppClient* client = [[ARDAppClient alloc] initWithDelegate:self];
402  [client connectToRoomWithId:roomId
403                     settings:[[ARDSettingsModel alloc] init]  // Use default settings.
404                   isLoopback:isLoopback];
405  _client = client;
406}
407
408#pragma mark - Private
409
410- (APPRTCMainView*)mainView {
411  return (APPRTCMainView*)self.view;
412}
413
414- (void)showAlertWithMessage:(NSString*)message {
415  dispatch_async(dispatch_get_main_queue(), ^{
416    NSAlert* alert = [[NSAlert alloc] init];
417    [alert setMessageText:message];
418    [alert runModal];
419  });
420}
421
422- (void)resetUI {
423  [_remoteVideoTrack removeRenderer:self.mainView.remoteVideoView];
424  [_localVideoTrack removeRenderer:self.mainView.localVideoView];
425  _remoteVideoTrack = nil;
426  _localVideoTrack = nil;
427  [self.mainView.remoteVideoView renderFrame:nil];
428  [self.mainView.localVideoView renderFrame:nil];
429}
430
431- (void)disconnect {
432  [self resetUI];
433  [_captureController stopCapture];
434  _captureController = nil;
435  [_client disconnect];
436}
437
438@end
439