ClassroomViewController.m 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  1. //
  2. // ClassroomViewController.m
  3. // MusicGradeExam
  4. //
  5. // Created by Kyle on 2020/7/10.
  6. // Copyright © 2020 DayaMusic. All rights reserved.
  7. //
  8. #import "ClassroomViewController.h"
  9. #import "ClassTitleView.h"
  10. #import "ClassroomMainContainer.h"
  11. #import "ClassVideoListView.h"
  12. #import "KSNormalAlertView.h"
  13. #import "RTCService.h"
  14. #import "Classroom.h"
  15. #import "ClassroomService.h"
  16. #import <MBProgressHUD/MBProgressHUD.h>
  17. #import "AppDelegate.h"
  18. #import "UIDevice+TFDevice.h"
  19. #import "KSRemoteUserManager.h"
  20. #import "LocalRenderManager.h"
  21. #import "NSDate+Transform.h"
  22. #import "UIViewController+zhStatusBarStyle.h"
  23. #import "KSTipsView.h"
  24. #import "RoomLoginHelper.h"
  25. #import "SubjectImageModel.h"
  26. #define TitleViewHeight (64)
  27. typedef NS_ENUM(NSInteger, KICKTYPE) {
  28. KICKTYPE_NONE,
  29. KICKTYPE_FINISH = 1,
  30. KICKTYPE_RECORD = 2,
  31. KICKTYPE_RESIGN = 3,
  32. };
  33. @interface ClassroomViewController ()<ClassTitleViewDelegate, RCRTCRoomEventDelegate, ClassroomDelegate, UIGestureRecognizerDelegate, RCRTCActivityMonitorDelegate, RTCServiceDelegate, ClassVideoListViewDelegate>
  34. @property (nonatomic, strong) ClassTitleView *titleView;
  35. @property (nonatomic, strong) ClassroomMainContainer *containerView;
  36. @property (nonatomic, strong) ClassVideoListView *videoListView;
  37. @property (nonatomic, strong) MBProgressHUD *hud;
  38. @property (nonatomic, strong) NSMutableArray<NSNumber*>* packetLossStore;
  39. @property (nonatomic, strong) UILabel *networkLabel;
  40. @property (nonatomic, assign) BOOL cancleAlert;
  41. @property (nonatomic, assign) BOOL hasShowAlert;
  42. @property (nonatomic, assign) BOOL isSwitchLine; // 是否线路切换
  43. @property (nonatomic, strong) KSTipsView *tipsView;
  44. @property (nonatomic, assign) KICKTYPE kickType; // 退出状态
  45. @property (nonatomic, strong) SubjectImageModel *guideImageModel;
  46. @property (nonatomic, assign) BOOL isSimulitionExam; // 是否模拟考试
  47. @end
  48. @implementation ClassroomViewController
  49. - (void)setRegistrationId:(NSString *)registrationId {
  50. _registrationId = registrationId;
  51. _isSimulitionExam = [registrationId containsString:@"S"] ? YES : NO;
  52. }
  53. #pragma mark - Life cycle
  54. - (void)viewWillAppear:(BOOL)animated {
  55. [super viewWillAppear:animated];
  56. self.zh_statusBarHidden = YES;
  57. // 切换到横屏
  58. AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
  59. delegate.allowAutoRotate = YES;
  60. [UIDevice switchNewOrientation:UIInterfaceOrientationLandscapeRight];
  61. [[UIApplication sharedApplication] setIdleTimerDisabled:YES];
  62. [RCRTCEngine sharedInstance].monitorDelegate = self;
  63. }
  64. - (void)viewWillDisappear:(BOOL)animated {
  65. [super viewWillDisappear:animated];
  66. // 竖屏
  67. AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
  68. delegate.allowAutoRotate = NO;
  69. [UIDevice switchNewOrientation:UIInterfaceOrientationPortrait];
  70. [[UIApplication sharedApplication] setIdleTimerDisabled:NO];
  71. self.zh_statusBarHidden = NO;
  72. }
  73. - (void)viewDidLoad {
  74. [super viewDidLoad];
  75. // Do any additional setup after loading the view.
  76. self.view.backgroundColor = [UIColor colorWithHexString:@"141414" alpha:1];
  77. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(leaveRoomAction) name:@"classroomLogout" object:nil];
  78. // 切换到横屏
  79. AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
  80. delegate.allowAutoRotate = YES;
  81. [UIDevice switchNewOrientation:UIInterfaceOrientationLandscapeRight];
  82. [self addSubviews];
  83. [self bindDelegates];
  84. [self publishStream];
  85. [self renderMainContainerView];
  86. [[RTCService sharedInstance] useSpeaker:YES];
  87. UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
  88. [self.view addGestureRecognizer:tapGes];
  89. tapGes.delegate = self;
  90. [self showRoleHud];
  91. self.kickType = KICKTYPE_NONE;
  92. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundQuitRoomAction) name:@"backgroundQuit" object:nil];
  93. }
  94. - (void)backgroundQuitRoomAction {
  95. self.isSwitchLine = NO;
  96. [[RoomLoginHelper sharedInstance] logout:^{
  97. } error:^(RCRTCCode code) {
  98. }];
  99. self.cancleAlert = YES;
  100. }
  101. #pragma mark ----- 账号异地登陆
  102. - (void)leaveRoomAction {
  103. [self.tipsView showTipsMessage:@"该账号在其他地方登录" inView:self.view];
  104. self.isSwitchLine = NO;
  105. [[RoomLoginHelper sharedInstance] logout:^{
  106. } error:^(RCRTCCode code) {
  107. }];
  108. self.cancleAlert = YES;
  109. [[NSNotificationCenter defaultCenter] postNotificationName:@"backLoginView" object:nil];
  110. }
  111. // 提示信息
  112. - (void)showMessage:(NSString *)message {
  113. MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
  114. [self.view bringSubviewToFront:hud];
  115. hud.removeFromSuperViewOnHide =YES;
  116. hud.mode = MBProgressHUDModeText;
  117. hud.label.text = message;
  118. hud.minSize = CGSizeMake(132.f, 60.0f);
  119. hud.label.textColor = [UIColor whiteColor];
  120. hud.bezelView.backgroundColor = [UIColor colorWithHexString:@"#000000" alpha:0.8];
  121. [hud hideAnimated:YES afterDelay:2];
  122. }
  123. - (void)tapGesture: (UITapGestureRecognizer *)tapGesture {
  124. self.titleView.isDisplay = YES;
  125. }
  126. -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
  127. if ([touch.view isDescendantOfView:self.videoListView]) {
  128. return NO;
  129. }
  130. if ([touch.view isDescendantOfView:self.containerView]) {
  131. [self tapGesture:(UITapGestureRecognizer *)gestureRecognizer];
  132. }
  133. return YES;
  134. }
  135. - (void)showRoleHud {
  136. Role role = [ClassroomService sharedService].currentRoom.currentMember.role;
  137. if(role == RoleAudience) {
  138. [self.tipsView showTipsMessage:@"你当前身份是旁听人,其他人看/听不见你" inView:self.view];
  139. [self performSelector:@selector(showOnlyYouHUD) withObject:nil afterDelay:5.0f];
  140. }else{
  141. [self showOnlyYouHUD];
  142. }
  143. }
  144. - (void)showOnlyYouHUD {
  145. if ([ClassroomService sharedService].currentRoom.memberList.count == 1) {
  146. [self.tipsView showTipsMessage:@"当前课堂只有你一人,你可以等待或离开" inView:self.view];
  147. }
  148. }
  149. #pragma mark --- ClassTitleViewDelegate
  150. - (void)classTitleView:(UIButton *)button didTapAtTag:(ClassTitleViewActionTag)tag {
  151. switch (tag) {
  152. case ClassTitleViewActionTagMicrophone:
  153. {
  154. if (button.selected == NO) {
  155. [KSNormalAlertView ks_showAlertWithTitle:@"确认关闭麦克风吗?" leftTitle:@"取消" rightTitle:@"确认" cancel:^{
  156. } confirm:^{
  157. button.selected = YES;
  158. if (self.isSimulitionExam) {
  159. [[RTCService sharedInstance] setMicrophoneDisable:YES];
  160. }
  161. else {
  162. [[ClassroomService sharedService] enableDevice:NO withType:DeviceTypeMicrophone];
  163. }
  164. }];
  165. }
  166. else {
  167. button.selected = NO;
  168. if (self.isSimulitionExam) {
  169. [[RTCService sharedInstance] setMicrophoneDisable:NO];
  170. }
  171. else {
  172. [[ClassroomService sharedService] enableDevice:YES withType:DeviceTypeMicrophone];
  173. }
  174. }
  175. }
  176. break;
  177. case ClassTitleViewActionTagCamera:
  178. {
  179. if (button.selected == NO) {
  180. [KSNormalAlertView ks_showAlertWithTitle:@"确认关闭摄像头吗?" leftTitle:@"取消" rightTitle:@"确认" cancel:^{
  181. } confirm:^{
  182. button.selected = YES;
  183. if (self.isSimulitionExam) {
  184. [[RTCService sharedInstance] setCameraDisable:YES];
  185. }
  186. else {
  187. [[ClassroomService sharedService] enableDevice:NO withType:DeviceTypeCamera];
  188. }
  189. }];
  190. }
  191. else {
  192. button.selected = NO;
  193. if (self.isSimulitionExam) {
  194. [[RTCService sharedInstance] setCameraDisable:NO];
  195. }
  196. else {
  197. [[ClassroomService sharedService] enableDevice:YES withType:DeviceTypeCamera];
  198. }
  199. }
  200. }
  201. break;
  202. case ClassTitleViewActionTagSwitchLine: // 切换线路
  203. {
  204. [KSNormalAlertView ks_showAlertWithTitle:@"确认切换线路吗?" leftTitle:@"取消" rightTitle:@"确认" cancel:^{
  205. } confirm:^{
  206. [self restartRoom];
  207. }];
  208. }
  209. break;
  210. case ClassTitleViewActionTagSwitchCamera:
  211. {
  212. [[RTCService sharedInstance] switchCamera];
  213. }
  214. break;
  215. case ClassTitleViewActionTagGuide: // 声部示意图
  216. {
  217. button.selected = !button.selected;
  218. self.containerView.hiddenGuideImage = button.selected;
  219. }
  220. break;
  221. case ClassTitleViewActionTagHangup:
  222. {
  223. [KSNormalAlertView ks_showAlertWithTitle:@"确认退出考场吗?" leftTitle:@"取消" rightTitle:@"确认" cancel:^{
  224. } confirm:^{
  225. SealClassLog(@"ActionTagHangup");
  226. self.hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
  227. [[RoomLoginHelper sharedInstance] logout:^{
  228. } error:^(RCRTCCode code) {
  229. [self.hud hideAnimated:YES];
  230. }];
  231. }];
  232. }
  233. break;
  234. default:
  235. break;
  236. }
  237. }
  238. // 判断是否需要退出房间
  239. - (void)refreshClassStatus {
  240. }
  241. #pragma mark ------ 退出RTC房间并重新加入
  242. - (void)restartRoom {
  243. self.isSwitchLine = YES;
  244. self.hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
  245. [[RoomLoginHelper sharedInstance] logout:^{
  246. } error:^(RCRTCCode code) {
  247. }];
  248. }
  249. - (void)reJoinRTCRoom {
  250. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  251. NSString *roomId = [RoomLoginHelper sharedInstance].roomId;
  252. NSString *userName = [RoomLoginHelper sharedInstance].userName;
  253. [[ClassroomService sharedService] joinClassroom:roomId userName:userName isAudience:NO success:^(Classroom * _Nonnull classroom) {
  254. self.isSwitchLine = NO;
  255. [self restartRTCConnection];
  256. } error:^(ErrorCode errorCode, NSString * _Nonnull message) {
  257. [self.tipsView showTipsMessage:@"切换线路失败!" inView:self.view];
  258. [self.hud hideAnimated:YES];
  259. self.isSwitchLine = NO;
  260. }];
  261. });
  262. }
  263. - (void)restartRTCConnection {
  264. [[RTCService sharedInstance] joinRongRTCRoom:[ClassroomService sharedService].currentRoom.roomId success:^(RCRTCRoom * _Nonnull room) {
  265. [self.hud hideAnimated:YES];
  266. [self.tipsView showTipsMessage:@"切换线路成功" inView:self.view];
  267. // 重置UI 重新发布音视频流
  268. [LocalRenderManager shareInstance].hadRenderMainView = NO;
  269. [self publishStream];
  270. [self bindDelegates];
  271. // 重置声部示意图
  272. self.titleView.guideBtn.selected = NO;
  273. self.containerView.hiddenGuideImage = NO;
  274. [self.titleView refreshTitleView];
  275. [RCRTCEngine sharedInstance].monitorDelegate = self;
  276. [[RTCService sharedInstance] useSpeaker:YES];
  277. [self renderMainContainerView];
  278. [self.videoListView reloadVideoList];
  279. } error:^(RCRTCCode code) {
  280. [self.hud hideAnimated:YES];
  281. [self.tipsView showTipsMessage:@"切换线路失败!" inView:self.view];
  282. }];
  283. }
  284. #pragma mark - ClassroomDelegate
  285. - (void)roomDidLeave {
  286. NSLog(@"roomDidLeave");
  287. if (_isSwitchLine) {
  288. if ([RTCService sharedInstance].rtcRoom) {
  289. [[KSRemoteUserManager shareInstance] removeAllUser];
  290. [[RTCService sharedInstance] leaveRongRTCRoom:[ClassroomService sharedService].currentRoom.roomId success:^{
  291. [self reJoinRTCRoom];
  292. } error:^(RCRTCCode code) {
  293. [self.hud hideAnimated:YES];
  294. [self.tipsView showTipsMessage:@"切换线路失败!" inView:self.view];
  295. self.isSwitchLine = NO;
  296. }];
  297. }
  298. }
  299. else if (_kickType != KICKTYPE_NONE) { // 退出房间,但是不dismiss
  300. [[KSRemoteUserManager shareInstance] removeAllUser];
  301. if ([RTCService sharedInstance].rtcRoom) {
  302. [[RTCService sharedInstance] leaveRongRTCRoom:[ClassroomService sharedService].currentRoom.roomId success:^{
  303. } error:^(RCRTCCode code) {
  304. }];
  305. }
  306. [self showKickAlert];
  307. }
  308. else {
  309. [self.hud hideAnimated:YES];
  310. [[KSRemoteUserManager shareInstance] removeAllUser];
  311. if ([RTCService sharedInstance].rtcRoom) {
  312. [[RTCService sharedInstance] leaveRongRTCRoom:[ClassroomService sharedService].currentRoom.roomId success:^{
  313. } error:^(RCRTCCode code) {
  314. }];
  315. }
  316. [self dismissClassroom];
  317. }
  318. }
  319. - (void)showKickAlert {
  320. if (_kickType == KICKTYPE_FINISH) {
  321. [KSNormalAlertView ks_showAlertWithTitle:@"您已完成考试,请耐心等待考试结果。" confirmTitle:@"退出考场" confirm:^{
  322. [self dismissClassroom];
  323. }];
  324. }
  325. else if (_kickType == KICKTYPE_RECORD) {
  326. // 弹窗提示去录播
  327. [KSNormalAlertView ks_showAlertWithTitle:@"当前状态无法正常完成在线直播考试\n主考官建议您录播上传考试视频。" leftTitle:@"取消" rightTitle:@"去录播" cancel:^{
  328. // 离开教室
  329. [self dismissClassroom];
  330. } confirm:^{
  331. // 去录播
  332. [self dismissClassroom];
  333. [[NSNotificationCenter defaultCenter] postNotificationName:RecordExamNotification object:nil];
  334. }];
  335. }
  336. else if (_kickType == KICKTYPE_RESIGN) {
  337. [KSNormalAlertView ks_showAlertWithTitle:@"当前状态无法正常完成考试,请检查设备及网络状态正常后再次签到考试。" confirmTitle:@"确定" confirm:^{
  338. // 离开教室
  339. [self dismissClassroom];
  340. }];
  341. }
  342. }
  343. - (void)dismissClassroom {
  344. [self.hud hideAnimated:YES];
  345. [ClassroomService sharedService].currentRoom = nil;
  346. [self dismissViewControllerAnimated:NO completion:^{
  347. for (UIView *view in [UIApplication sharedApplication].keyWindow.subviews) {
  348. if ([view isKindOfClass:[KSNormalAlertView class]]) {
  349. [view removeFromSuperview];
  350. }
  351. }
  352. [self.titleView stopDurationTimer];
  353. }];
  354. }
  355. - (void)memberDidJoin:(RoomMember *)member {
  356. NSLog(@"memberDidJoin %@",member);
  357. if(member.role == RoleMainTeacher || member.role == RoleAssistantTeacher) {
  358. [self.videoListView reloadVideoList];
  359. }
  360. if (member.role == RoleMainTeacher || member.role == RoleAssistantTeacher) {
  361. [self.tipsView showTipsMessage:[NSString stringWithFormat:@"老师%@已经进入考场", member.name] inView:self.view];
  362. }
  363. }
  364. - (void)memberDidLeave:(RoomMember *)member {
  365. NSLog(@"memberDidLeave %@",member);
  366. // 此处移除
  367. [[KSRemoteUserManager shareInstance] removeUser:member.userId];
  368. if(member.role == RoleMainTeacher || member.role == RoleAssistantTeacher) {
  369. [self.videoListView reloadVideoList];
  370. }
  371. }
  372. - (void)memberDidKick:(RoomMember *)member {
  373. NSLog(@"memberDidKick %@",member);
  374. RoomMember *curMember = [ClassroomService sharedService].currentRoom.currentMember;
  375. [[ClassroomService sharedService].currentRoom removeMemeber:member];
  376. if ([curMember.userId isEqualToString:member.userId]) {
  377. self.kickType = KICKTYPE_FINISH;
  378. [[RoomLoginHelper sharedInstance] logout:^{
  379. } error:^(RCRTCCode code) {
  380. }];
  381. return;
  382. }
  383. [[KSRemoteUserManager shareInstance] removeUser:member.userId];
  384. Role curRole = [ClassroomService sharedService].currentRoom.currentMember.role;
  385. if (curRole == RoleMainTeacher || curRole == RoleAssistantTeacher) {
  386. [self.tipsView showTipsMessage:[NSString stringWithFormat:@"你已将%@移出考场", member.name] inView:self.view];
  387. }
  388. [self.videoListView reloadVideoList];
  389. if (self.containerView.member.role == member.role) {
  390. [self.containerView cancelRenderView];
  391. }
  392. }
  393. - (void)memberRecordAction:(RoomMember *)member {
  394. NSLog(@"memberRecordAction %@", member);
  395. RoomMember *curMember = [ClassroomService sharedService].currentRoom.currentMember;
  396. if ([curMember.userId isEqualToString:member.userId]) {
  397. self.kickType = KICKTYPE_RECORD;
  398. [[RoomLoginHelper sharedInstance] logout:^{
  399. } error:^(RCRTCCode code) {
  400. }];
  401. }
  402. }
  403. // 不播放声音列表
  404. - (void)memberIgnoreAction:(RoomMember *)member {
  405. NSLog(@"memberRecordAction %@", member);
  406. NSString *shieldUserId = member.shieldUserId;
  407. NSArray *memberArray = [shieldUserId componentsSeparatedByString:@","];
  408. [[RTCService sharedInstance] unPlayRemoteUserAudioStream:memberArray];
  409. }
  410. - (void)memberNoFinishAction:(RoomMember *)member {
  411. NSLog(@"memberRecordAction %@", member);
  412. RoomMember *curMember = [ClassroomService sharedService].currentRoom.currentMember;
  413. if ([curMember.userId isEqualToString:member.userId]) {
  414. self.kickType = KICKTYPE_RESIGN;
  415. [[RoomLoginHelper sharedInstance] logout:^{
  416. } error:^(RCRTCCode code) {
  417. }];
  418. }
  419. }
  420. - (void)errorDidOccur:(ErrorCode)code {
  421. NSLog(@"errorDidOccur %@",@(code));
  422. [self.hud hideAnimated:YES];
  423. if (code != ErrorCodeSuccess) {
  424. if (code == ErrorCodeOverMaxUserCount) {
  425. [self.tipsView showTipsMessage:@"超过人数限制" inView:self.view];
  426. }else {
  427. [self.tipsView showTipsMessage:@"操作失败,请稍后再试" inView:self.view];
  428. }
  429. }
  430. }
  431. - (void)deviceDidEnable:(BOOL)enable type:(DeviceType)type forUser:(RoomMember *)member operator:(nonnull NSString *)operatorId {
  432. NSLog(@"deviceDidEnable devicetype:%@ enable:%@ memeber:%@",@(type),@(enable),member);
  433. RoomMember *curMember = [ClassroomService sharedService].currentRoom.currentMember;
  434. NSString *hudMessage = @"";
  435. //只有老师和自己才有提示
  436. if ((curMember.role == RoleMainTeacher || curMember.role == RoleAssistantTeacher) && ![curMember.userId isEqualToString:operatorId]) {
  437. if (type == DeviceTypeCamera) {
  438. hudMessage = !enable ? [NSString stringWithFormat:NSLocalizedStringFromTable(@"SetCameraClose", @"SealClass", nil),member.name] : [NSString stringWithFormat:NSLocalizedStringFromTable(@"SetCameraOpen", @"SealClass", nil),member.name];
  439. } else if (type == DeviceTypeMicrophone) {
  440. hudMessage = !enable ? [NSString stringWithFormat:NSLocalizedStringFromTable(@"SetMicorophoneClose", @"SealClass", nil),member.name] : [NSString stringWithFormat:NSLocalizedStringFromTable(@"SetMicorophoneOpen", @"SealClass", nil),member.name];
  441. }
  442. else if (type == DeviceTypeMusicMode) {
  443. hudMessage = !enable ? [NSString stringWithFormat:@"%@的音乐模式已关闭",member.name] : [NSString stringWithFormat:@"%@的音乐模式已开启",member.name];
  444. }
  445. [self.tipsView showTipsMessage:hudMessage inView:self.view];
  446. } else {
  447. if ([curMember.userId isEqualToString:member.userId]) {
  448. if (type == DeviceTypeCamera) { // 摄像头
  449. // 非自己操作才需要做提示
  450. if(![curMember.userId isEqualToString:operatorId]){
  451. hudMessage = !enable ? @"老师已关闭你的摄像头" : NSLocalizedStringFromTable(@"CameraOpend", @"SealClass", nil);
  452. [self.tipsView showTipsMessage:hudMessage inView:self.view];
  453. }
  454. [[RTCService sharedInstance] setCameraDisable:!enable];
  455. } else if (type == DeviceTypeMicrophone) { // 麦克风
  456. // 非自己操作才需要做提示
  457. if(![curMember.userId isEqualToString:operatorId]){
  458. hudMessage = !enable ? @"老师已关闭你的麦克风" : NSLocalizedStringFromTable(@"MicorophoneOpend", @"SealClass", nil);
  459. [self.tipsView showTipsMessage:hudMessage inView:self.view];
  460. }
  461. [[RTCService sharedInstance] setMicrophoneDisable:!enable];
  462. }
  463. else if (type == DeviceTypeMusicMode) {
  464. // 非自己操作才需要做提示
  465. if(![curMember.userId isEqualToString:operatorId]) {
  466. }
  467. [self changeModeAction:enable];
  468. }
  469. else if (type == DeviceTypeHandup) {
  470. }
  471. }
  472. }
  473. if (type == DeviceTypeCamera && [curMember.userId isEqualToString:member.userId]) {
  474. // 延迟调用,防止刷新出现灰屏
  475. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  476. [self renderMainContainerView];
  477. });
  478. }
  479. [self.titleView refreshTitleView];
  480. }
  481. //助教请求用户打开设备,助教关闭用户设备没有回调。
  482. - (void)deviceDidInviteEnable:(DeviceType)type ticket:(NSString *)ticket {
  483. NSLog(@"deviceDidInviteEnable devicetype:%@ ticket:%@ ",@(type),ticket);
  484. if (type == DeviceTypeCamera) {
  485. [KSNormalAlertView ks_showAlertWithTitle:@"老师邀请你打开摄像头" leftTitle:@"拒绝" rightTitle:@"同意" cancel:^{
  486. [[ClassroomService sharedService] rejectEnableDevice:ticket];
  487. } confirm:^{
  488. [[ClassroomService sharedService] approveEnableDevice:ticket];
  489. }];
  490. }else if (type == DeviceTypeMicrophone) {
  491. [KSNormalAlertView ks_showAlertWithTitle:@"老师邀请你打开麦克风" leftTitle:@"拒绝" rightTitle:@"同意" cancel:^{
  492. [[ClassroomService sharedService] rejectEnableDevice:ticket];
  493. } confirm:^{
  494. [[ClassroomService sharedService] approveEnableDevice:ticket];
  495. }];
  496. }
  497. else {
  498. [[ClassroomService sharedService] approveEnableDevice:ticket];
  499. }
  500. }
  501. //只有助教能收到这个回调,可以不在这里处理文字,因为设备的回调还会走
  502. - (void)deviceInviteEnableDidApprove:(RoomMember *)member type:(DeviceType)type {
  503. NSLog(@"deviceInviteEnableDidApprove devicetype:%@ member:%@ ",@(type),member);
  504. }
  505. //拒绝只有助教能收到这个回调,且只走这个回调
  506. - (void)deviceInviteEnableDidReject:(RoomMember *)member type:(DeviceType)type {
  507. NSLog(@"deviceInviteEnableDidReject devicetype:%@ member:%@ ",@(type),member);
  508. [self.tipsView showTipsMessage:[NSString stringWithFormat:@"%@拒绝了你的邀请",member.name] inView:self.view];
  509. }
  510. #pragma mark - private method
  511. - (void)bindDelegates {
  512. self.titleView.delegate = self;
  513. [[RTCService sharedInstance] setRTCRoomDelegate:self];
  514. [RTCService sharedInstance].delegate = self;
  515. [ClassroomService sharedService].classroomDelegate = self;
  516. }
  517. - (void)addSubviews {
  518. [self.view addSubview:self.containerView];
  519. [self.view addSubview:self.titleView];
  520. [self.view addSubview:self.videoListView];
  521. [self.view bringSubviewToFront:self.titleView];
  522. }
  523. - (CGRect)mainContainerViewFrame {
  524. CGFloat x = iPhoneXSafeTopMargin;
  525. CGFloat y = 0;
  526. CGFloat width = UIScreenWidth - x - iPhoneXSafeBottomMargin;
  527. CGFloat height = UIScreenHeight - y;
  528. return CGRectMake(x, y, width, height);
  529. }
  530. - (void)publishStream {
  531. if ([ClassroomService sharedService].currentRoom.currentMember.role != RoleAudience) {
  532. [[RTCService sharedInstance] publishLocalUserDefaultAVStream];
  533. }
  534. }
  535. - (void)renderMainContainerView {
  536. // 主屏显示学生
  537. [self.containerView containerViewRenderView:[ClassroomService sharedService].currentRoom.currentMember];
  538. }
  539. - (void)whiteboardDidDisplay:(NSString *)boardId {
  540. NSLog(@"whiteboardDidDisplay %@ ",boardId);
  541. [self renderMainContainerView];
  542. }
  543. - (void)sharedScreenDidDisplay:(NSString *)userId {
  544. NSLog(@"sharedScreenDidDisplay %@ ",userId);
  545. [self renderMainContainerView];
  546. }
  547. - (void)studentDidDisplay:(NSString *)studentId {
  548. NSLog(@"studentDidDisplay %@", studentId);
  549. [self renderMainContainerView];
  550. }
  551. - (void)teacherDidDisplay {
  552. NSLog(@"teacherDidDisplay %@ ",[ClassroomService sharedService].currentRoom.teacher);
  553. [self renderMainContainerView];
  554. }
  555. - (void)noneDidDisplay {
  556. NSLog(@"noneDidDisplay");
  557. [self renderMainContainerView];
  558. }
  559. #pragma mark --- RTCServiceDelegate 自己发布流成功回调
  560. - (void)successPublishLocalStream {
  561. }
  562. #pragma mark - RongRTCRoomDelegate
  563. - (void)didPublishStreams:(NSArray <RCRTCInputStream *>*)streams {
  564. // 当用户非正常退出的时候,如果再次进入需要重新订阅流
  565. RCRTCInputStream *firstStream = [streams lastObject];
  566. if ([[KSRemoteUserManager shareInstance] isContentUser:firstStream.userId]) {
  567. [[KSRemoteUserManager shareInstance] removeUser:firstStream.userId];
  568. }
  569. for (RCRTCInputStream *stream in streams) {
  570. if (stream.mediaType == RTCMediaTypeVideo) {
  571. [self.videoListView updateUserVideo:stream.userId];
  572. }
  573. }
  574. }
  575. - (void)didConnectToStream:(RCRTCInputStream *)stream {
  576. if (stream.streamId && stream.streamId.length > 0) {
  577. NSLog(@"didConnectToStream userId:%@ streamID:%@",stream.userId,stream.streamId);
  578. }
  579. }
  580. - (void)didReportFirstKeyframe:(RCRTCInputStream *)stream {
  581. if (stream.streamId && stream.streamId.length > 0) {
  582. }
  583. }
  584. #pragma mark ----- ClassVideoListViewDelegate
  585. - (void)videoListView:(ClassVideoListView *)view didTap:(RoomMember *)member {
  586. self.titleView.isDisplay = YES;
  587. }
  588. #pragma mark - Getters & setters
  589. - (SubjectImageModel *)guideImageModel {
  590. if (!_guideImageModel) {
  591. _guideImageModel = [[SubjectImageModel alloc] init];
  592. _guideImageModel.subjectName = self.subjectName;
  593. }
  594. return _guideImageModel;
  595. }
  596. - (KSTipsView *)tipsView {
  597. if (!_tipsView) {
  598. _tipsView = [[KSTipsView alloc] initWithFrame:CGRectZero];
  599. }
  600. return _tipsView;
  601. }
  602. - (ClassTitleView *)titleView {
  603. if (!_titleView) {
  604. _titleView = [[ClassTitleView alloc] initWithFrame:CGRectMake(iPhoneXSafeTopMargin, 0, kScreenWidth - iPhoneXSafeTopMargin - iPhoneXSafeBottomMargin, TitleViewHeight)];
  605. _titleView.hasGuideImage = self.guideImageModel.hasImage;
  606. }
  607. return _titleView;
  608. }
  609. - (ClassVideoListView *)videoListView {
  610. if(!_videoListView) {
  611. CGFloat width = 0.0f;
  612. CGFloat height = 0.0f;
  613. height = kScreenHeight - 10;
  614. width = height / 3.0f * 4;
  615. if (width > kScreenWidth - 160) {
  616. width = kScreenWidth - 160;
  617. }
  618. _videoListView = [[ClassVideoListView alloc] initWithFrame:CGRectMake(width + iPhoneXSafeTopMargin + 5, 10, kScreenWidth - (width + 10 + iPhoneXSafeTopMargin) - iPhoneXSafeBottomMargin, height)];
  619. _videoListView.delegate = self;
  620. _videoListView.isSimulitionExam = self.isSimulitionExam;
  621. }
  622. return _videoListView;
  623. }
  624. - (ClassroomMainContainer *)containerView {
  625. if (!_containerView) {
  626. _containerView = [[ClassroomMainContainer alloc] initWithFrame:[self mainContainerViewFrame]];
  627. _containerView.subjectImage = self.guideImageModel.guideImage;
  628. }
  629. return _containerView;
  630. }
  631. #pragma mark ---- 网络监测
  632. - (void)didReportStatForm:(RCRTCStatisticalForm *)form {
  633. const CGFloat lossRateBase = 0.15;
  634. if (self.packetLossStore.count + form.sendStats.count > 10) {
  635. NSInteger diff = (self.packetLossStore.count + form.sendStats.count) - 10;
  636. [self.packetLossStore removeObjectsInRange:NSMakeRange(0,diff)];
  637. }
  638. for (RCRTCStreamStat* stat in form.sendStats) {
  639. [self.packetLossStore addObject:@(stat.packetLoss)];
  640. }
  641. float totalSum = 0;
  642. for (NSNumber* lossRate in self.packetLossStore) {
  643. totalSum += [lossRate floatValue];
  644. }
  645. if (totalSum > 0 && self.packetLossStore.count) {
  646. float avgLoss = totalSum / self.packetLossStore.count;
  647. [self showNetworkingStatus:avgLoss];
  648. if (avgLoss > lossRateBase) { // 网络质量不佳
  649. dispatch_async(dispatch_get_main_queue(), ^{
  650. [self.view addSubview:self.networkLabel];
  651. [self.view bringSubviewToFront:self.networkLabel];
  652. self.networkLabel.hidden = NO;
  653. self.networkLabel.text = @"当前通话连接质量不佳";
  654. });
  655. }
  656. else { // 正常
  657. dispatch_async(dispatch_get_main_queue(), ^{
  658. self.networkLabel.hidden = YES;
  659. self.networkLabel.text = @"";
  660. [self.networkLabel removeFromSuperview];
  661. });
  662. }
  663. }
  664. }
  665. - (void)showNetworkingStatus:(float)packageLose {
  666. dispatch_async(dispatch_get_main_queue(), ^{
  667. if (packageLose < 0.05) {
  668. self.titleView.netStatus = NetWorkingStatus_Good;
  669. }
  670. else if (packageLose <= 0.1) {
  671. self.titleView.netStatus = NetWorkingStatus_Well;
  672. }
  673. else if (packageLose <= 0.15) {
  674. self.titleView.netStatus = NetWorkingStatus_Bad;
  675. }
  676. else {
  677. self.titleView.netStatus = NetWorkingStatus_Poor;
  678. }
  679. });
  680. }
  681. - (NSMutableArray<NSNumber*>*)packetLossStore {
  682. if (!_packetLossStore) {
  683. _packetLossStore = [[NSMutableArray alloc] initWithCapacity:10];
  684. }
  685. return _packetLossStore;
  686. }
  687. - (UILabel *)networkLabel {
  688. if (!_networkLabel) {
  689. _networkLabel = [[UILabel alloc] initWithFrame:CGRectMake((kScreenWidth - 200) / 2.0f, (kScreenHeight - 60) / 2.0f, 200, 60)];
  690. _networkLabel.backgroundColor = HexRGBAlpha(0x000000, 0.8f);
  691. _networkLabel.textColor = HexRGB(0xffffff);
  692. _networkLabel.textAlignment = NSTextAlignmentCenter;
  693. _networkLabel.font = [UIFont systemFontOfSize:14.0f];
  694. _networkLabel.layer.cornerRadius = 5.0f;
  695. }
  696. return _networkLabel;
  697. }
  698. #pragma mark ---- 修改声音模式
  699. - (void)changeModeAction:(BOOL)enable {
  700. if (enable) {
  701. [[RCRTCEngine sharedInstance].defaultAudioStream changeMusicPlayMode:RCRTCAudioScenarioMusicSingleNotePlay];
  702. }
  703. else {
  704. [[RCRTCEngine sharedInstance].defaultAudioStream changeMusicPlayMode:RCRTCAudioScenarioMusicNormalPlay];
  705. }
  706. }
  707. /*
  708. #pragma mark - Navigation
  709. // In a storyboard-based application, you will often want to do a little preparation before navigation
  710. - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  711. // Get the new view controller using [segue destinationViewController].
  712. // Pass the selected object to the new view controller.
  713. }
  714. */
  715. @end