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