diff --git a/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/hungup.imageset/Contents.json b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/hungup.imageset/Contents.json new file mode 100644 index 0000000000..a57430f1d3 --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/hungup.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "hungup.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/hungup.imageset/hungup.png b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/hungup.imageset/hungup.png new file mode 100644 index 0000000000..6a02817dc5 Binary files /dev/null and b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/hungup.imageset/hungup.png differ diff --git a/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall.imageset/Contents.json b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall.imageset/Contents.json new file mode 100644 index 0000000000..1a9d084a3b --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "videocall.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall.imageset/videocall.png b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall.imageset/videocall.png new file mode 100644 index 0000000000..29e1152d9c Binary files /dev/null and b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall.imageset/videocall.png differ diff --git a/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall_out.imageset/Contents.json b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall_out.imageset/Contents.json new file mode 100644 index 0000000000..9d58da1be9 --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall_out.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "videocall_out.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall_out.imageset/videocall_out.png b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall_out.imageset/videocall_out.png new file mode 100644 index 0000000000..3364c2bfc8 Binary files /dev/null and b/iOS/TUIKitDemo/TUIKitDemo/Assets.xcassets/videocall_out.imageset/videocall_out.png differ diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/ChatViewController+video.h b/iOS/TUIKitDemo/TUIKitDemo/Chat/ChatViewController+video.h new file mode 100644 index 0000000000..adaab6a954 --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/ChatViewController+video.h @@ -0,0 +1,20 @@ +// +// ChatViewController+video.h +// TUIKitDemo +// +// Created by xcoderliu on 9/30/19. +// Copyright © 2019 Tencent. All rights reserved. +// + + +#import "VideoCallCell.h" +#import "ChatViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ChatViewController (video) +- (BOOL)videoCallTimeOut: (TIMMessage*)message; +- (TUIMessageCellData *)chatController:(TUIChatController *)controller onNewVideoCallMessage:(TIMMessage *)msg; +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/ChatViewController+video.m b/iOS/TUIKitDemo/TUIKitDemo/Chat/ChatViewController+video.m new file mode 100644 index 0000000000..78a3762529 --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/ChatViewController+video.m @@ -0,0 +1,66 @@ +// +// ChatViewController+video.m +// TUIKitDemo +// +// Created by xcoderliu on 9/30/19. +// Copyright © 2019 Tencent. All rights reserved. +// + +#import "ChatViewController+video.h" +#import "TCUtil.h" +#import "THelper.h" +#import "VideoCallManager.h" + +@implementation ChatViewController (video) + +- (TUIMessageCellData *)chatController:(TUIChatController *)controller onNewVideoCallMessage:(TIMMessage *)msg { + TIMElem *elem = [msg getElem:0]; + if([elem isKindOfClass:[TIMCustomElem class]]) { + NSDictionary *param = [TCUtil jsonData2Dictionary:[(TIMCustomElem *)elem data]]; + UInt32 state = [param[@"videoState"] unsignedIntValue]; + NSString* user = param[@"requestUser"]; + if ( state == VIDEOCALL_REQUESTING || + state == VIDEOCALL_USER_CONNECTTING ) { //请求或开始连接 + + return nil; + } + + BOOL isOutGoing = msg.isSelf; + if (state == VIDEOCALL_USER_REJECT || + state == VIDEOCALL_USER_ONCALLING) { //由我发起请求,对方发送结果 + isOutGoing = !isOutGoing; + } + + if (user != nil) { + NSString *currentUser = [[VideoCallManager shareInstance] currentUserIdentifier]; + isOutGoing = (user == currentUser); + } + + VideoCallCellData *cellData = [[VideoCallCellData alloc] initWithDirection:isOutGoing + ? MsgDirectionOutgoing : MsgDirectionIncoming]; + cellData.roomID = [param[@"roomID"] unsignedIntValue]; + cellData.isSelf = msg.isSelf; + cellData.videoState = state; + cellData.requestUser = user; + NSNumber *duration = param[@"duration"]; + if (duration != nil) { + cellData.duration = [duration unsignedIntValue]; + } else { + cellData.duration = 0; + } + cellData.avatarUrl = [NSURL URLWithString:[[TIMFriendshipManager sharedInstance] querySelfProfile].faceURL]; + + return cellData; + } + return nil; +} + +- (BOOL)videoCallTimeOut: (TIMMessage*)message { + NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:message.timestamp]; + if ( time >= VIDEOCALL_TIMEOUT) { + return YES; + } + return NO; +} + +@end diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/Meeting/VideoCallManager+videoMeeting.h b/iOS/TUIKitDemo/TUIKitDemo/Chat/Meeting/VideoCallManager+videoMeeting.h new file mode 100644 index 0000000000..9a73348dcb --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/Meeting/VideoCallManager+videoMeeting.h @@ -0,0 +1,20 @@ +// +// VideoCallManager+videoMeeting.h +// TUIKitDemo +// +// Created by xcoderliu on 10/14/19. +// Copyright © 2019 Tencent. All rights reserved. +// + + + +#import "VideoCallManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface VideoCallManager (videoMeeting) +- (void)_enterMeetingRoom; +- (void)_quitMeetingRoom; +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/Meeting/VideoCallManager+videoMeeting.m b/iOS/TUIKitDemo/TUIKitDemo/Chat/Meeting/VideoCallManager+videoMeeting.m new file mode 100644 index 0000000000..ac21bb115c --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/Meeting/VideoCallManager+videoMeeting.m @@ -0,0 +1,166 @@ +// +// VideoCallManager+videoMeeting.m +// TUIKitDemo +// +// Created by xcoderliu on 10/14/19. +// Copyright © 2019 Tencent. All rights reserved. +// + +#import "VideoCallManager+videoMeeting.h" +#import "AppDelegate.h" +#import "GenerateTestUserSig.h" +#import +#import +#import + +@interface VideoCallManager () + +@end + +typedef enum : NSUInteger { + TRTC_IDLE, // SDK 没有进入视频通话状态 + TRTC_ENTERED, // SDK 视频通话进行中 +} TRTCStatus; + +@implementation VideoCallManager (videoMeeting) +- (void)_enterMeetingRoom { + // TRTC相关参数设置 + TRTCParams *param = [[TRTCParams alloc] init]; + param.sdkAppId = SDKAPPID; + param.userId = [self currentUserIdentifier]; + param.roomId = self.currentRoomID; + param.userSig = [GenerateTestUserSig genTestUserSig:[self currentUserIdentifier]]; + param.privateMapKey = @""; + if (self.localVideoView) { + [self.localVideoView removeFromSuperview]; + } + if (self.remoteVideoView) { + [self.remoteVideoView removeFromSuperview]; + } + self.localVideoView = [[UIView alloc] init]; + self.remoteVideoView = [[UIView alloc] init]; + self.hungUP = [[UIButton alloc] init]; + self.localVideoView.backgroundColor = [UIColor grayColor]; + self.remoteVideoView.backgroundColor = [UIColor grayColor]; + UIWindow *window = [[[UIApplication sharedApplication] windows] lastObject]; + [self.remoteVideoView setFrame:window.rootViewController.view.bounds]; + [self.localVideoView setFrame:window.rootViewController.view.bounds]; + + [self.hungUP addTarget:self action:@selector(userQuit) forControlEvents:UIControlEventTouchUpInside]; + [self.hungUP setImage:[UIImage imageNamed:@"hungup"] forState:UIControlStateNormal]; + [self.hungUP setFrame:CGRectMake((window.rootViewController.view.bounds.size.width - 60) / 2, window.rootViewController.view.bounds.size.height - 120, 60, 60)]; + [self.hungUP.layer setCornerRadius:60 / 2]; + [self.hungUP setClipsToBounds:YES]; + [window.rootViewController.view addSubview:self.remoteVideoView]; + [window.rootViewController.view addSubview:self.localVideoView]; + [window.rootViewController.view addSubview:self.hungUP]; + [[TRTCCloud sharedInstance] setDelegate:self]; + [[TRTCCloud sharedInstance] enterRoom:param appScene:(TRTCAppSceneVideoCall)]; + [[TRTCCloud sharedInstance] startLocalPreview:YES view:self.localVideoView]; + [[TRTCCloud sharedInstance] muteLocalVideo:NO]; + [[TRTCCloud sharedInstance] startLocalAudio]; + [self setRoomStatus:TRTC_ENTERED]; +} + +-(void)_quitMeetingRoom { + [self setRoomStatus:TRTC_IDLE]; + [[TRTCCloud sharedInstance] stopLocalPreview]; + [[TRTCCloud sharedInstance] exitRoom]; + if (self.localVideoView) { + [self.localVideoView removeFromSuperview]; + self.localVideoView = nil; + } + if (self.remoteVideoView) { + [self.remoteVideoView removeFromSuperview]; + self.remoteVideoView = nil; + } + if (self.hungUP) { + [self.hungUP removeFromSuperview]; + self.hungUP = nil; + } +} + +#pragma mark - config + +- (NSString *)hmac:(NSString *)plainText +{ + const char *cKey = [@"61cbf613d0cea4b302958e39c7b74acaaed0956fe8c494eda1c45912c324ecab" + cStringUsingEncoding:NSUTF8StringEncoding]; + const char *cData = [plainText cStringUsingEncoding:NSUTF8StringEncoding]; + + unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH]; + + CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), cHMAC); + + NSData *HMACData = [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)]; + return [HMACData base64EncodedStringWithOptions:0]; +} + +- (NSString *)base64URL:(NSData *)data +{ + NSString *result = [data base64EncodedStringWithOptions:0]; + NSMutableString *final = [[NSMutableString alloc] init]; + const char *cString = [result cStringUsingEncoding:NSUTF8StringEncoding]; + for (int i = 0; i < result.length; ++ i) { + char x = cString[i]; + switch(x){ + case '+': + [final appendString:@"*"]; + break; + case '/': + [final appendString:@"-"]; + break; + case '=': + [final appendString:@"_"]; + break; + default: + [final appendFormat:@"%c", x]; + break; + } + } + return final; +} + +/** + * 防止iOS锁屏:如果视频通话进行中,则方式iPhone进入锁屏状态 + */ +- (void)setRoomStatus:(TRTCStatus)roomStatus { + + switch (roomStatus) { + case TRTC_IDLE: + [[UIApplication sharedApplication] setIdleTimerDisabled:NO]; + break; + case TRTC_ENTERED: + [[UIApplication sharedApplication] setIdleTimerDisabled:YES]; + break; + default: + break; + } +} + +#pragma mark - callback of TRTC +- (void)onEnterRoom:(NSInteger)result { + NSLog(@"%ld",(long)result); +} +- (void)onUserVideoAvailable:(NSString *)userId available:(BOOL)available { + if (available && userId != [self currentUserIdentifier]) { + [[TRTCCloud sharedInstance] startRemoteView:userId view:self.remoteVideoView]; + } +} + +- (void)onFirstVideoFrame:(NSString *)userId streamType:(TRTCVideoStreamType)streamType width:(int)width height:(int)height { + if (userId != [self currentUserIdentifier]) { //对方视频 + [UIView animateWithDuration:0.6 animations:^{ + [self.localVideoView setFrame:CGRectMake(self.remoteVideoView.bounds.size.width - 100 - 18, 20, 100, 100.0 / 9.0 * 16.0)]; + }]; + } +} + +- (void)onUserExit:(NSString *)userId reason:(NSInteger)reason { + NSLog(@"%ld",(long)reason); +} + +- (void)userQuit { + [self quitRoom:NO]; +} +@end diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCell.h b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCell.h new file mode 100644 index 0000000000..f1f9ae736d --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCell.h @@ -0,0 +1,20 @@ +// +// VideoCallCell.h +// TUIKitDemo +// +// Created by xcoderliu on 9/29/19. +// Copyright © 2019 Tencent. All rights reserved. +// + +#import "TUITextMessageCell.h" +#import "VideoCallCellData.h" + +NS_ASSUME_NONNULL_BEGIN +typedef void(^videoCellClickBlock)(void); + +@interface VideoCallCell : TUITextMessageCell +- (void)fillWithData:(VideoCallCellData *)data; +@property (nonatomic,copy,nullable)videoCellClickBlock videlClick; +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCell.m b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCell.m new file mode 100644 index 0000000000..9ec7b0984a --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCell.m @@ -0,0 +1,46 @@ +#import "VideoCallCell.h" +#import +#import +#import + +@implementation VideoCallCell + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + UITapGestureRecognizer *singleFingerTap = + [[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(handleSingleTap:)]; + [self.container addGestureRecognizer:singleFingerTap]; + } + return self; +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated { + [super setSelected:selected animated:animated]; + + // Configure the view for the selected state +} + +-(void)fillWithData:(VideoCallCellData *)data { + [data setAvatarImage:DefaultAvatarImage]; + [data setAvatarUrl:[NSURL URLWithString:@""]]; + [data setName:data.requestUser]; + [super fillWithData:data]; + [[TIMFriendshipManager sharedInstance] getUsersProfile:@[data.requestUser] forceUpdate:NO succ:^(NSArray *profiles) { + [data setAvatarUrl:[NSURL URLWithString:profiles[0].faceURL]]; + [data setName:profiles[0].nickname]; + } fail:^(int code, NSString *msg) { + + }]; +} + +- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer +{ + if (self.videlClick) { + self.videlClick(); + } +} + +@end diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCellData.h b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCellData.h new file mode 100644 index 0000000000..946445b962 --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCellData.h @@ -0,0 +1,35 @@ +// +// VideoCallCellData.h +// TUIKitDemo +// +// Created by xcoderliu on 9/29/19. +// Copyright © 2019 Tencent. All rights reserved. +// + +#import "TUITextMessageCellData.h" + +NS_ASSUME_NONNULL_BEGIN + +#define VIDEOCALL_TIMEOUT 20 //超时时间 + +typedef NS_ENUM(UInt32, videoCallState) +{ + VIDEOCALL_REQUESTING = 0, //请求发起 - 不展示UI 状态 action + VIDEOCALL_USER_CANCEL, //用户取消 (视频发起端:用户取消 接收端: 未接听) 结果 result + VIDEOCALL_USER_REJECT, //用户拒绝 (视频发起端:对方已拒绝 接收端: 未接听) 结果 result + VIDEOCALL_USER_NO_RESP, //用户未应答 (视频发起端:对方未应答 接收端: 未接听) 结果 result + VIDEOCALL_USER_CONNECTTING, //用户同意并进行连接 (视频发起端:enterRoom 接收端:enterRoom) 状态 action + VIDEOCALL_USER_HANUGUP, //用户挂断 (视频发起端:已结束 接收端: 已结束) 结果 result + VIDEOCALL_USER_ONCALLING //用户通话中 (视频发起端:对方通话中 接收端: 未接听) 结果 result +}; + +@interface VideoCallCellData : TUITextMessageCellData +@property (nonatomic, strong) NSString* requestUser; +@property (nonatomic, assign) UInt32 roomID; +@property (nonatomic, assign) videoCallState videoState; +@property (nonatomic, assign) UInt32 duration; +@property (nonatomic, assign) BOOL isSelf; +- (BOOL)isOutGoingResult; +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCellData.m b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCellData.m new file mode 100644 index 0000000000..1c8b72deba --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallCellData.m @@ -0,0 +1,133 @@ +// +// VideoCallCellData.m +// TUIKitDemo +// +// Created by xcoderliu on 9/29/19. +// Copyright © 2019 Tencent. All rights reserved. +// + +#import "VideoCallCellData.h" +#import "VideoCallManager.h" + +@implementation VideoCallCellData + +- (CGSize)contentSize { + NSString *result = @"已结束"; + BOOL isOutGoing = [self isOutGoingResult]; + if (isOutGoing) { + if (_videoState == VIDEOCALL_USER_CANCEL) { + result = @"已取消"; + } else if (_videoState == VIDEOCALL_USER_REJECT) { + result = @"对方已拒绝"; + } else if (_videoState == VIDEOCALL_USER_NO_RESP) { + result = @"对方无应答"; + } else if (_videoState == VIDEOCALL_USER_HANUGUP) { + result = [NSString stringWithFormat:@"已结束 %@",[self getMMSSFromSS:_duration]]; + } + } else { + if (_videoState == VIDEOCALL_USER_CANCEL) { + result = @"未接通"; + } else if (_videoState == VIDEOCALL_USER_REJECT) { + result = @"未接通"; + } else if (_videoState == VIDEOCALL_USER_NO_RESP) { + result = @"未接通"; + } else if (_videoState == VIDEOCALL_USER_HANUGUP) { + result = [NSString stringWithFormat:@"已结束 %@",[self getMMSSFromSS:_duration]]; + } + } + NSString *content = isOutGoing ? + [NSString stringWithFormat:@"%@ [videoCallOut]",result] : + [NSString stringWithFormat:@"[videoCall] %@",result]; + self.content = content; + return [super contentSize]; +} + +- (NSAttributedString *)formatMessageString:(NSString *)text +{ + //先判断text是否存在 + if (text == nil || text.length == 0) { + NSLog(@"TTextMessageCell formatMessageString failed , current text is nil"); + return [[NSMutableAttributedString alloc] initWithString:@""]; + } + //1、创建一个可变的属性字符串 + NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:text]; + + //2、通过正则表达式来匹配字符串 + NSString *regex_emoji = @"\\[[a-zA-Z0-9\\/\\u4e00-\\u9fa5]+\\]"; //匹配表情 + + NSError *error = nil; + NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error]; + if (!re) { + NSLog(@"%@", [error localizedDescription]); + return attributeString; + } + + NSArray *resultArray = [re matchesInString:text options:0 range:NSMakeRange(0, text.length)]; + + //3、获取所有的表情以及位置 + //用来存放字典,字典中存储的是图片和图片对应的位置 + NSMutableArray *imageArray = [NSMutableArray arrayWithCapacity:resultArray.count]; + //根据匹配范围来用图片进行相应的替换 + for(NSTextCheckingResult *match in resultArray) { + //获取数组元素中得到range + NSRange range = [match range]; + //获取原字符串中对应的值 + NSString *subStr = [text substringWithRange:range]; + + if ([subStr hasPrefix:@"[videoCall"]) { + //新建文字附件来存放我们的图片,iOS7才新加的对象 + NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init]; + //给附件添加图片 + textAttachment.image = [UIImage imageNamed: [subStr hasPrefix:@"[videoCallOut"] ? @"videocall_out" : @"videocall"]; + //调整一下图片的位置,如果你的图片偏上或者偏下,调整一下bounds的y值即可 + textAttachment.bounds = CGRectMake(0, -(self.textFont.lineHeight-self.textFont.pointSize*0.3)/2, self.textFont.pointSize*1.6, self.textFont.pointSize*1.6); + //把附件转换成可变字符串,用于替换掉源字符串中的表情文字 + NSAttributedString *imageStr = [NSAttributedString attributedStringWithAttachment:textAttachment]; + //把图片和图片对应的位置存入字典中 + NSMutableDictionary *imageDic = [NSMutableDictionary dictionaryWithCapacity:2]; + [imageDic setObject:imageStr forKey:@"image"]; + [imageDic setObject:[NSValue valueWithRange:range] forKey:@"range"]; + //把字典存入数组中 + [imageArray addObject:imageDic]; + break; + } + } + + //4、从后往前替换,否则会引起位置问题 + for (int i = (int)imageArray.count -1; i >= 0; i--) { + NSRange range; + [imageArray[i][@"range"] getValue:&range]; + //进行替换 + [attributeString replaceCharactersInRange:range withAttributedString:imageArray[i][@"image"]]; + } + + [attributeString addAttribute:NSFontAttributeName value:self.textFont range:NSMakeRange(0, attributeString.length)]; + + return attributeString; +} + +/// 当前的结果是对方请求视频还是己方 +- (BOOL)isOutGoingResult { + BOOL isOutGoing = self.isSelf; + UInt32 state = self.videoState; + if (state == VIDEOCALL_USER_REJECT || state == VIDEOCALL_USER_ONCALLING) { //由我发起请求,对方发送结果 + isOutGoing = !isOutGoing; + } + + if (self.requestUser != nil) { + NSString *currentUser = [[VideoCallManager shareInstance] currentUserIdentifier]; + isOutGoing = (self.requestUser == currentUser); + } + return isOutGoing; +} + +-(NSString *)getMMSSFromSS:(UInt32)seconds { + //format of minute + NSString *str_minute = [NSString stringWithFormat:@"%02d",seconds / 60]; + //format of second + NSString *str_second = [NSString stringWithFormat:@"%02d",seconds % 60]; + //format of time + NSString *format_time = [NSString stringWithFormat:@"%@:%@",str_minute,str_second]; + return format_time; +} +@end diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallManager.h b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallManager.h new file mode 100644 index 0000000000..b70712718d --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallManager.h @@ -0,0 +1,43 @@ +// +// VideoCallManager.h +// TUIKitDemo +// +// Created by xcoderliu on 9/30/19. +// Copyright © 2019 Tencent. All rights reserved. +// + +#import +#import "TUIChatController.h" +#import "VideoCallCell.h" + +@class ChatViewController; + +NS_ASSUME_NONNULL_BEGIN + +@interface VideoCallManager : NSObject ++(instancetype) shareInstance; +@property (nonatomic, strong, nullable)UIButton *hungUP; +@property (nonatomic, strong, nullable) UIView* remoteVideoView; +@property (nonatomic, strong, nullable) UIView* localVideoView; +@property (nonatomic, strong, nullable) ChatViewController* currentChatVC; +/// 当前roomID +@property (nonatomic,assign) UInt32 currentRoomID; + +//当前用户Identifier +- (NSString*) currentUserIdentifier; + +/// 连接参数 +@property (nonatomic, strong) TUIConversationCellData *conversationData; + +//从聊天界面发起请求 +- (void)videoCall:(TUIChatController *)chatController; + +//新消息处理 +- (void)onNewVideoCallMessage:(TIMMessage *)msg; + +/// 离开会议室 +/// @param passive 被动离开 +- (void)quitRoom:(BOOL)passive; +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallManager.m b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallManager.m new file mode 100644 index 0000000000..4947d3ce6b --- /dev/null +++ b/iOS/TUIKitDemo/TUIKitDemo/Chat/VideoCall/VideoCallManager.m @@ -0,0 +1,311 @@ +// +// VideoCallManager.m +// TUIKitDemo +// +// Created by xcoderliu on 9/30/19. +// Copyright © 2019 Tencent. All rights reserved. +// + +#import "VideoCallManager.h" +#import "TCUtil.h" +#import "VideoCallCellData.h" +#import "THelper.h" +#import "VideoCallManager+videoMeeting.h" +#import "ChatViewController.h" +#import +#import + +@interface VideoCallManager () +@property (nonatomic, strong, nullable) UIAlertController *requestAlert; +@property (nonatomic, strong, nullable) UIAlertController *actionAlert; +/// 正在视频通过的过程中 +@property (nonatomic,assign) BOOL isOnCalling; +/// 主动发起视频请求roomID +@property (nonatomic,assign) UInt32 lastRequestRoomID; +/// 当前roomID 请求者 +@property (nonatomic,assign) NSString* currentRoomRequestUser; +/// 视频请求是否返回结果 +@property (nonatomic, strong) NSMutableArray* getResultRoomIDs; +/// 进入会议室时间 +@property (nonatomic, strong, nullable) NSDate *enterRoomDate; +@end + +@implementation VideoCallManager +static VideoCallManager* _instance = nil; + ++(instancetype) shareInstance +{ + static dispatch_once_t onceToken ; + dispatch_once(&onceToken, ^{ + _instance = [[self alloc] init] ; + [_instance setIsOnCalling:NO]; + [_instance setLastRequestRoomID:0]; + [_instance setCurrentRoomID:0]; + [_instance setGetResultRoomIDs:[NSMutableArray arrayWithCapacity:1]]; + [_instance setConversationData:[[TUIConversationCellData alloc] init]]; + }) ; + + return _instance ; +} + +- (void)onNewVideoCallMessage:(TIMMessage *)msg { + TIMElem *elem = [msg getElem:0]; + if([elem isKindOfClass:[TIMCustomElem class]]) { + + NSDictionary *param = [TCUtil jsonData2Dictionary:[(TIMCustomElem *)elem data]]; + UInt32 state = [param[@"videoState"] unsignedIntValue]; + + if ( state == VIDEOCALL_REQUESTING || state == VIDEOCALL_USER_CONNECTTING ) { //请求或开始连接 + [self handActionMessage:msg videoState:state]; + } else { + [self handResultMessage:msg videoState:state]; + } + } +} + +- (BOOL)videoCallTimeOut: (TIMMessage*)message { + NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:message.timestamp]; + if ( time >= VIDEOCALL_TIMEOUT) { + return YES; + } + return NO; +} + +- (void)handActionMessage:(TIMMessage *)msg videoState:(videoCallState)state { + @weakify(self); + TIMElem *elem = [msg getElem:0]; + NSDictionary *param = [TCUtil jsonData2Dictionary:[(TIMCustomElem *)elem data]]; + UInt32 roomID = [param[@"roomID"] unsignedIntValue]; + NSString* user = param[@"requestUser"]; + + if(state == VIDEOCALL_REQUESTING) { + if (_isOnCalling || [self videoCallTimeOut:msg]) { //正在通话过程中 //或者超时了 + // FIXME: should send msg that is on calling , which user's ui should show other is on calling + return; + } + //test code + [msg getSenderProfile:^(TIMUserProfile *profile) { //处理申请消息 A -> B B处理 是否接收视频 + @strongify(self); + + self.conversationData.convType = TIM_C2C; + self.conversationData.convId = profile.identifier; + + UIAlertController * alertController = [UIAlertController alertControllerWithTitle:@"" message:[NSString stringWithFormat:@"%@来电",user] preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"接听" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + [self acceptVideoCall:roomID requestUser:user Accept:YES]; + }]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"拒绝" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { + [self acceptVideoCall:roomID requestUser:user Accept:NO]; + }]; + + [alertController addAction:cancelAction]; + [alertController addAction:okAction]; + + UIWindow *window = [[[UIApplication sharedApplication] windows] lastObject]; + [window.rootViewController presentViewController:alertController animated:YES completion:nil]; + NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:msg.timestamp]; + time = MIN(VIDEOCALL_TIMEOUT, time); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((VIDEOCALL_TIMEOUT - time) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [alertController dismissViewControllerAnimated:YES completion:nil]; + }); + self.actionAlert = alertController; + }]; + + } else if (state == VIDEOCALL_USER_CONNECTTING) { + [self.getResultRoomIDs addObject:@(roomID)]; + if (_lastRequestRoomID == roomID) { + [self dismissRequestAlert]; + [self enterVideoRoom:roomID requestUser:user]; + } + } + +} + +- (void)acceptVideoCall:(UInt32)roomID requestUser:(NSString*)user Accept:(BOOL)accept { + if (!accept) { + [self sendVideoCallResult:roomID requestUser:user state:VIDEOCALL_USER_REJECT]; + self.isOnCalling = NO; + } else { + [self sendVideoCallAction:roomID requestUser:user state:VIDEOCALL_USER_CONNECTTING]; + [self enterVideoRoom:roomID requestUser:user]; + self.isOnCalling = YES; + } +} + +- (void)handResultMessage:(TIMMessage *)msg videoState:(videoCallState)state { + TIMElem *elem = [msg getElem:0]; + NSDictionary *param = [TCUtil jsonData2Dictionary:[(TIMCustomElem *)elem data]]; + UInt32 roomID = [param[@"roomID"] unsignedIntValue]; + [self.getResultRoomIDs addObject:@(roomID)]; + if(!msg.isSelf) { //对方回应 + if (state == VIDEOCALL_USER_REJECT) { //对方拒绝 + [self dismissRequestAlert]; + _lastRequestRoomID = 0; + _isOnCalling = NO; + _currentRoomID = 0; + } + else if (state == VIDEOCALL_USER_CANCEL) { //对方取消 + [self.actionAlert dismissViewControllerAnimated:YES completion:nil]; + self.actionAlert = nil; + _lastRequestRoomID = 0; + _isOnCalling = NO; + _currentRoomID = 0; + } + else if (state == VIDEOCALL_USER_HANUGUP) { //对方挂断 + [self quitRoom: YES]; + } + } else { //本方 + + } +} + +- (void)videoCall:(TUIChatController *)chatController { + if (_isOnCalling) { + [self quitRoom: NO]; + } + + UInt32 roomID = arc4random(); + [self sendVideoCallAction:roomID requestUser:[self currentUserIdentifier] state:VIDEOCALL_REQUESTING]; + [self enterVideoWaitting:roomID]; +} + +- (void)sendVideoCallAction:(UInt32)roomID requestUser:(NSString*)user state:(videoCallState)videoState { + TIMMessage *imMsg = [[TIMMessage alloc] init]; + TIMCustomElem * custom_elem = [[TIMCustomElem alloc] init]; + custom_elem.data = [TCUtil dictionary2JsonData:@{@"roomID":@(roomID), @"version":@(2), @"videoState":@(videoState), @"requestUser":user}]; + [imMsg addElem:custom_elem]; + TIMConversation *conv = [[TIMManager sharedInstance] getConversation:self.conversationData.convType receiver:self.conversationData.convId]; + [conv sendMessage:imMsg succ:^{ + if (videoState == VIDEOCALL_REQUESTING) { //发起请求需要隐藏 + [imMsg remove]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(VIDEOCALL_TIMEOUT * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ //超时发送超时消息 + if (![self.getResultRoomIDs containsObject:@(roomID)]) { //没有任何结果 发送超时消息 + [self sendVideoCallResult:roomID requestUser:user state:VIDEOCALL_USER_NO_RESP]; + self.isOnCalling = NO; + [self dismissRequestAlert]; + } else { + + } + }); + } + } fail:^(int code, NSString *desc) { //失败应该展示结果 + dispatch_async(dispatch_get_main_queue(), ^{ + [THelper makeToastError:code msg:desc]; + }); + }]; +} + +- (void)sendVideoCallResult:(UInt32)roomID requestUser:(NSString*)user state:(videoCallState)videoState { + TIMMessage *imMsg = [[TIMMessage alloc] init]; + TIMCustomElem * custom_elem = [[TIMCustomElem alloc] init]; + NSMutableDictionary *resultDict = [NSMutableDictionary dictionaryWithDictionary:@{@"roomID":@(roomID), @"version":@(2), @"videoState":@(videoState), @"requestUser":user}]; + UInt32 duration = 0; + if (videoState == VIDEOCALL_USER_HANUGUP && _enterRoomDate != nil) { + NSTimeInterval secondsBetween = [[NSDate date] timeIntervalSinceDate:_enterRoomDate]; + duration = MAX(1, secondsBetween); + [resultDict setObject:@(secondsBetween) forKey:@"duration"]; + } + custom_elem.data = [TCUtil dictionary2JsonData:resultDict]; + [imMsg addElem:custom_elem]; + if (self.currentChatVC && + self.currentChatVC.conversationData.convType == self.conversationData.convType && + self.currentChatVC.conversationData.convId == self.conversationData.convId) { + BOOL isOutGoing = YES; + if (videoState == VIDEOCALL_USER_REJECT || + videoState == VIDEOCALL_USER_ONCALLING) { //由我发起请求,对方发送结果 + isOutGoing = !isOutGoing; + } + + if (user != nil) { + NSString *currentUser = [self currentUserIdentifier]; + isOutGoing = (user == currentUser); + } + + VideoCallCellData *cellResult = [[VideoCallCellData alloc] initWithDirection:isOutGoing + ? MsgDirectionOutgoing : MsgDirectionIncoming]; + cellResult.isSelf = imMsg.isSelf; + cellResult.requestUser = user; + cellResult.roomID = roomID; + cellResult.videoState = videoState; + cellResult.innerMessage = imMsg; + cellResult.duration = duration; + [self.currentChatVC sendMessage:cellResult]; + + } else { + TIMConversation *conv = [[TIMManager sharedInstance] getConversation:self.conversationData.convType + receiver:self.conversationData.convId]; + [conv sendMessage:imMsg succ:^{ + NSLog(@"success"); + } fail:^(int code, NSString *desc) { //失败应该展示结果 + dispatch_async(dispatch_get_main_queue(), ^{ + [THelper makeToastError:code msg:desc]; + }); + }]; + } +} + +- (void)enterVideoWaitting: (UInt32)roomID { + _isOnCalling = YES; + _lastRequestRoomID = roomID; + UIAlertController * alertController = [UIAlertController alertControllerWithTitle:@"" message:[NSString stringWithFormat:@"正在请求通话"] preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { + [self sendVideoCallResult:roomID requestUser:[self currentUserIdentifier] state:VIDEOCALL_USER_CANCEL]; + [self.getResultRoomIDs addObject:@(roomID)]; + self.isOnCalling = NO; + [self dismissRequestAlert]; + }]; + + [alertController addAction:cancelAction]; + + UIWindow *window = [[[UIApplication sharedApplication] windows] lastObject]; + [window.rootViewController presentViewController:alertController animated:YES completion:nil]; + self.requestAlert = alertController; +} + +- (void)enterVideoRoom: (UInt32)roomID requestUser:(NSString*)user { + _currentRoomID = roomID; + _currentRoomRequestUser = user; + [self dismissRequestAlert]; + + if (_lastRequestRoomID != 0 && roomID == _lastRequestRoomID) { //当前角色请求并进入 + + } else { + + } + + _enterRoomDate = [NSDate date]; + [self _enterMeetingRoom]; +} + +/// 离开会议室 +/// @param passive 被动离开 +- (void)quitRoom:(BOOL)passive { + [self _quitMeetingRoom]; + _lastRequestRoomID = 0; + _isOnCalling = NO; + [self dismissRequestAlert]; + if (_currentRoomID != 0 && !passive) { //主动断开发送结果消息 + [self sendVideoCallResult:_currentRoomID requestUser:self.currentRoomRequestUser state:VIDEOCALL_USER_HANUGUP]; + } + _currentRoomID = 0; +} + +- (void)dismissRequestAlert { + [[VideoCallManager shareInstance].requestAlert dismissViewControllerAnimated:YES completion:nil]; + [VideoCallManager shareInstance].requestAlert = nil; +} + +- (NSString*) currentUserIdentifier { + NSString *user = [[TIMManager sharedInstance] getLoginUser]; + if (user.length > 0) { + return user; + } + int iRandom = arc4random() % 1000; + return [NSString stringWithFormat:@"%d",iRandom]; +} + +@end