AmazonChimeSDKDemo/AmazonChimeSDKDemoObjC/ViewControllerObjC.m (330 lines of code) (raw):
//
// ViewControllerObjC.m
// AmazonChimeSDKDemo
//
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
#import "ViewControllerObjC.h"
@interface ViewControllerObjC ()
@end
@implementation ViewControllerObjC
- (void)viewDidLoad {
[super viewDidLoad];
[self updateUIWithMeetingStarted:NO];
self.logger = [[ConsoleLogger alloc] initWithName:@"ViewControllerObjC"
level:LogLevelINFO];
self.versionLabel.text = [NSString stringWithFormat:@"amazon-chime-sdk-ios@%@", [Versioning sdkVersion]];
}
- (IBAction)joinMeeting:(id)sender {
NSString* meetingId = [self formatInput:self.meetingIDText.text];
NSString* name = [self formatInput:self.nameText.text];
if (!meetingId.length || !name.length) {
[self showAlertIn:self withMessage:@"MeetingID and Name cannot be empty." withDelay:0];
return;
}
[self.joinButton setEnabled:NO];
NSCharacterSet *set = [NSCharacterSet URLQueryAllowedCharacterSet];
NSString *url = [[NSString stringWithFormat:@"%sjoin?title=%@&name=%@®ion=%s", SERVER_URL, meetingId, name, SERVER_REGION] stringByAddingPercentEncodingWithAllowedCharacters:set];
[self makeHttpRequest:url withMethod:@"POST" withData:nil withCompletionBlock:^(NSData *data, NSError *error) {
__weak typeof(self) weakSelf = self;
if (error != nil) {
[self showAlertIn:self
withMessage:[NSString stringWithFormat:@"Failed to join meeting, error: %@", error.localizedDescription]
withDelay:0];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.joinButton setEnabled:YES];
});
return;
}
if (data != nil) {
// Parse meeting join data from JSON
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
NSDictionary *joinInfoDict = [json objectForKey:@"JoinInfo"];
NSDictionary *meetingInfoDict = [[joinInfoDict objectForKey:@"Meeting"] objectForKey:@"Meeting"];
NSString *externalMeetingId = [meetingInfoDict objectForKey:@"ExternalMeetingId"];
NSString *meetingId = [meetingInfoDict objectForKey:@"MeetingId"];
NSString *mediaRegion = [meetingInfoDict objectForKey:@"MediaRegion"];
NSDictionary *mediaPlacementDict = [meetingInfoDict objectForKey:@"MediaPlacement"];
NSString *audioFallbackUrl = [mediaPlacementDict objectForKey:@"AudioFallbackUrl"];
NSString *audioHostUrl = [mediaPlacementDict objectForKey:@"AudioHostUrl"];
NSString *turnControlUrl = [mediaPlacementDict objectForKey:@"TurnControlUrl"];
NSString *signalingUrl = [mediaPlacementDict objectForKey:@"SignalingUrl"];
NSDictionary *meetingFeaturesDict = [meetingInfoDict objectForKey:@"MeetingFeatures"];
NSDictionary *videoFeaturesDict = [meetingFeaturesDict objectForKey:@"Video"];
NSString *videoMaxResolution = [videoFeaturesDict objectForKey:@"MaxResolution"];
NSDictionary *contentFeaturesDict = [meetingFeaturesDict objectForKey:@"Content"];
NSString *contentMaxResolution = [contentFeaturesDict objectForKey:@"MaxResolution"];
NSDictionary *attendeeInfoDict = [[joinInfoDict objectForKey:@"Attendee"] objectForKey:@"Attendee"];
NSString *attendeeId = [attendeeInfoDict objectForKey:@"AttendeeId"];
NSString *externalUserId = [attendeeInfoDict objectForKey:@"ExternalUserId"];
NSString *joinToken = [attendeeInfoDict objectForKey:@"JoinToken"];
// Initialize meeting session through AmazonChimeSDK
MediaPlacement *mediaPlacement = [[MediaPlacement alloc] initWithAudioFallbackUrl:audioFallbackUrl
audioHostUrl:audioHostUrl
signalingUrl:signalingUrl
turnControlUrl:turnControlUrl];
MeetingFeatures *meetingFeatures= [[MeetingFeatures alloc] initWithVideo:videoMaxResolution
content:contentMaxResolution];
Meeting *meeting = [[Meeting alloc] initWithExternalMeetingId:externalMeetingId
mediaPlacement:mediaPlacement
meetingFeatures:meetingFeatures
mediaRegion:mediaRegion
meetingId:meetingId
primaryMeetingId:nil];
CreateMeetingResponse *createMeetingResponse = [[CreateMeetingResponse alloc] initWithMeeting:meeting];
Attendee *attendee = [[Attendee alloc] initWithAttendeeId:attendeeId
externalUserId:externalUserId
joinToken:joinToken];
CreateAttendeeResponse *createAttendeeResponse = [[CreateAttendeeResponse alloc] initWithAttendee:attendee];
MeetingSessionConfiguration *meetingSessionConfiguration = [[MeetingSessionConfiguration alloc]
initWithCreateMeetingResponse:createMeetingResponse
createAttendeeResponse:createAttendeeResponse];
self.meetingSession = [[DefaultMeetingSession alloc] initWithConfiguration:meetingSessionConfiguration
logger:self.logger];
[self startAudioVideo];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.joinButton setEnabled:YES];
});
}
}];
}
- (void)startAudioVideo {
if (self.meetingSession == nil) {
[self.logger errorWithMsg:@"meetingSession is not initialized"];
return;
}
NSError* error = nil;
[self.meetingSession.audioVideo addEventAnalyticsObserverWithObserver:self];
BOOL started = [self.meetingSession.audioVideo startAndReturnError:&error];
if (started && error == nil) {
[self.logger infoWithMsg:@"ObjC meeting session was started successfully"];
[self updateUIWithMeetingStarted:YES];
[self.meetingSession.audioVideo addRealtimeObserverWithObserver:self];
[self.meetingSession.audioVideo addMetricsObserverWithObserver:self];
[self.meetingSession.audioVideo addVideoTileObserverWithObserver:self];
DefaultActiveSpeakerPolicy *policy = [DefaultActiveSpeakerPolicy new];
[self.meetingSession.audioVideo addActiveSpeakerObserverWithPolicy:policy
observer:self];
[self startVideoClient];
} else {
NSString *errorMsg = [NSString stringWithFormat:@"Failed to start meeting, error: %@", error.description];
[self.logger errorWithMsg:errorMsg];
// Handle missing permission error
if ([error.domain isEqual:@"AmazonChimeSDK.PermissionError"]) {
AVAudioSessionRecordPermission permissionStatus = [[AVAudioSession sharedInstance] recordPermission];
if (permissionStatus == AVAudioSessionRecordPermissionUndetermined) {
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
if (granted) {
[self.logger infoWithMsg:@"Audio permission granted"];
// Retry after permission is granted
[self startAudioVideo];
}
else {
[self.logger infoWithMsg:@"Audio permission not granted"];
[self showAlertIn:self
withMessage:@"Please go to Settings and grant audio permission to this app."
withDelay:0];
}
}];
} else if (permissionStatus == AVAudioSessionRecordPermissionDenied) {
[self.logger errorWithMsg:@"User did not grant permission, should redirect to Settings"];
[self showAlertIn:self
withMessage:@"Please go to Settings and grant audio permission to this app."
withDelay:0];
}
} else {
// Uncaught error
[self showAlertIn:self
withMessage:errorMsg
withDelay:0];
}
}
}
- (void)startVideoClient {
if (self.meetingSession == nil) {
[self.logger errorWithMsg:@"meetingSession is not initialized"];
return;
}
[self.logger infoWithMsg:@"Starting video client and enabling local and remote video..."];
[self.meetingSession.audioVideo startRemoteVideo];
[self.logger infoWithMsg:@"Remote video was started successfully"];
NSError* error = nil;
BOOL started = [self.meetingSession.audioVideo startLocalVideoAndReturnError:&error];
if (started && error == nil) {
[self.logger infoWithMsg:@"Self video was started successfully"];
} else {
NSString *errorMsg = [NSString stringWithFormat:@"Failed to start self video, error: %@", error.description];
[self.logger errorWithMsg:errorMsg];
// Handle missing permission error
if ([error.domain isEqual:@"AmazonChimeSDK.PermissionError"]) {
AVAuthorizationStatus permissionStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (permissionStatus == AVAuthorizationStatusNotDetermined) {
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
if (granted) {
[self.logger infoWithMsg:@"Camera permission granted"];
// Retry after permission is granted
[self startVideoClient];
}
else {
[self.logger infoWithMsg:@"Camera permission not granted"];
[self showAlertIn:self
withMessage:@"Please go to Settings and grant camera permission to this app."
withDelay:0];
}
}];
} else if (permissionStatus == AVAuthorizationStatusDenied) {
[self.logger errorWithMsg:@"User did not grant permission, should redirect to Settings"];
[self showAlertIn:self
withMessage:@"Please go to Settings and grant camera permission to this app."
withDelay:0];
}
} else {
// Uncaught error
[self showAlertIn:self
withMessage:errorMsg
withDelay:0];
}
}
}
- (IBAction)leaveMeeting:(id)sender {
[self.meetingSession.audioVideo removeRealtimeObserverWithObserver:self];
[self.meetingSession.audioVideo removeMetricsObserverWithObserver:self];
[self.meetingSession.audioVideo removeVideoTileObserverWithObserver:self];
[self.meetingSession.audioVideo removeActiveSpeakerObserverWithObserver:self];
[self.meetingSession.audioVideo removeEventAnalyticsObserverWithObserver:self];
[self.meetingSession.audioVideo stop];
[self updateUIWithMeetingStarted:NO];
}
- (void)updateUIWithMeetingStarted:(BOOL) started {
dispatch_async(dispatch_get_main_queue(), ^{
[self.titleLabel setText:started ? @"Meeting started" : @"Join a meeting"];
[self.meetingIDText setEnabled:!started];
[self.nameText setEnabled:!started];
[self.joinButton setHidden:started];
[self.leaveButton setHidden:!started];
[self.selfVideoView setHidden:!started];
[self.remoteVideoView setHidden:!started];
});
}
- (NSString *)formatInput:(NSString *)text {
return [[text stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet] stringByReplacingOccurrencesOfString:@" " withString:@"+"];
}
- (void)showAlertIn:(UIViewController *)controller withMessage:(NSString *)message withDelay:(double)seconds {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController* alert = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
if (seconds <= 0) {
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {}];
[alert addAction:defaultAction];
} else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[controller dismissViewControllerAnimated:YES completion:nil];
});
}
[controller presentViewController:alert animated:YES completion:nil];
});
}
- (void)makeHttpRequest:(NSString *)url withMethod:(NSString *)method withData:(NSData *)data withCompletionBlock:(void (^)(NSData *, NSError *))completion {
NSURL *serverUrl = [[NSURL alloc] initWithString:url];
NSURLSession *session = [NSURLSession sharedSession];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:serverUrl];
request.HTTPMethod = [method uppercaseString];
if ([request.HTTPMethod isEqualToString:@"POST"] && data != nil) {
request.HTTPBody = data;
}
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error != nil) {
[self showAlertIn:self withMessage:error.localizedDescription withDelay:0];
completion(nil, error);
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
if (httpResponse.statusCode < 200 || httpResponse.statusCode > 299) {
[self showAlertIn:self withMessage:[NSString stringWithFormat:@"Received status code %ld", (long)httpResponse.statusCode] withDelay:0];
completion(nil,
[NSError errorWithDomain:@"AmazonChimeSDKDemoObjC"
code:httpResponse.statusCode
userInfo:nil]);
} else {
completion(data, nil);
}
}];
[task resume];
}
# pragma mark - RealtimeObserver
- (void)attendeesDidJoinWithAttendeeInfo:(NSArray<AttendeeInfo *> * _Nonnull)attendeeInfo {
for (id currentAttendeeInfo in attendeeInfo) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Attendee %@ joined", [currentAttendeeInfo attendeeId]]];
}
}
- (void)attendeesDidLeaveWithAttendeeInfo:(NSArray<AttendeeInfo *> * _Nonnull)attendeeInfo {
for (id currentAttendeeInfo in attendeeInfo) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Attendee %@ left", [currentAttendeeInfo attendeeId]]];
}
}
- (void)attendeesDidMuteWithAttendeeInfo:(NSArray<AttendeeInfo *> * _Nonnull)attendeeInfo {
for (id currentAttendeeInfo in attendeeInfo) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Attendee %@ muted", [currentAttendeeInfo attendeeId]]];
}
}
- (void)attendeesDidUnmuteWithAttendeeInfo:(NSArray<AttendeeInfo *> * _Nonnull)attendeeInfo {
for (id currentAttendeeInfo in attendeeInfo) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Attendee %@ unmuted", [currentAttendeeInfo attendeeId]]];
}
}
- (void)signalStrengthDidChangeWithSignalUpdates:(NSArray<SignalUpdate *> * _Nonnull)signalUpdates {
for (id currentSignalUpdate in signalUpdates) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Attendee %@ signalStrength changed to %lu", [[currentSignalUpdate attendeeInfo] attendeeId], (unsigned long)[currentSignalUpdate signalStrength]]];
}
}
- (void)volumeDidChangeWithVolumeUpdates:(NSArray<VolumeUpdate *> * _Nonnull)volumeUpdates {
for (id currentVolumeUpdate in volumeUpdates) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Attendee %@ volumeLevel changed to %lu", [[currentVolumeUpdate attendeeInfo] attendeeId], (unsigned long)[currentVolumeUpdate volumeLevel]]];
}
}
- (void)attendeesDidDropWithAttendeeInfo:(NSArray<AttendeeInfo *> * _Nonnull)attendeeInfo {
for (id currentAttendeeInfo in attendeeInfo) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Attendee %@ dropped", [currentAttendeeInfo attendeeId]]];
}
}
# pragma mark - MetricsObserver
- (void)metricsDidReceiveWithMetrics:(NSDictionary *)metrics {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Media metrics have been received: %@", metrics]];
}
# pragma mark - VideoTileObserver
- (void)videoTileDidAddWithTileState:(VideoTileState *)tileState {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Adding Video Tile tileId: %ld, attendeeId: %@", (long)tileState.tileId, tileState.attendeeId]];
DefaultVideoRenderView *renderView = nil;
if (tileState.isLocalTile) {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Binding self video"]];
renderView = self.selfVideoView;
// Flip front camera video on rendering
if (self.meetingSession.audioVideo.getActiveCamera.type == MediaDeviceTypeVideoFrontCamera) {
renderView.mirror = YES;
}
} else {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Binding remote video"]];
renderView = self.remoteVideoView;
}
[self.meetingSession.audioVideo bindVideoViewWithVideoView:renderView tileId:tileState.tileId];
}
- (void)videoTileDidRemoveWithTileState:(VideoTileState *)tileState {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Removing Video Tile tileId: %ld, attendeeId: %@", (long)tileState.tileId, tileState.attendeeId]];
[self.meetingSession.audioVideo unbindVideoViewWithTileId:tileState.tileId];
if (![tileState isLocalTile]) {
[self.remoteVideoView resetImage];
}
}
- (void)videoTileDidPauseWithTileState:(VideoTileState *)tileState {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Video Tile paused: tileId: %ld, attendeeId: %@", (long)tileState.tileId, tileState.attendeeId]];
}
- (void)videoTileDidResumeWithTileState:(VideoTileState *)tileState {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Video Tile resumed: tileId: %ld, attendeeId: %@", (long)tileState.tileId, tileState.attendeeId]];
}
- (void)videoTileSizeDidChangeWithTileState:(VideoTileState *)tileState {
[self.logger infoWithMsg:[NSString stringWithFormat:@"Video Tile size changed: tileId: %ld, attendeeId: %@", (long)tileState.tileId, tileState.attendeeId]];
}
# pragma mark - ActiveSpeakerObserver
- (void)activeSpeakerScoreDidChangeWithScores:(NSDictionary<AttendeeInfo *, NSNumber *> * _Nonnull)scores {
[self.logger infoWithMsg:@"activeSpeakerScoreDidChangeWithScores callback invoked"];
}
- (void)activeSpeakerDidDetectWithAttendeeInfo:(NSArray<AttendeeInfo *> * _Nonnull)attendeeInfo {
[self.logger infoWithMsg:@"activeSpeakerDidDetectWithAttendeeInfo callback invoked"];
}
- (NSInteger)scoresCallbackIntervalMs {
return 5000;
}
- (NSString*)observerId {
return [NSUUID new].UUIDString;
}
# pragma mark - EventAnalyticObserver
- (void)eventDidReceiveWithName:(enum EventName)name attributes:(NSDictionary *)attributes {
[self.logger infoWithMsg:[NSString stringWithFormat:@"%lu %@\n", (unsigned long)name, [attributes toJsonString]]];
}
@end