// // SellyVideoCallConferenceController.m // SellyCloudSDK_Example // // Created by Caleb on 11/11/25. // Copyright © 2025 Caleb. All rights reserved. // #import "SellyVideoCallConferenceController.h" #import #import "FUManager.h" #import "UIView+SellyCloud.h" #import "SLSVideoGridView.h" #import "TokenGenerator.h" @interface SellyVideoCallConferenceController () // 你的 WebRTC 会话对象 @property (nonatomic, strong) SellyRTCSession *session; @property (nonatomic, assign) BOOL localVideoEnable; @property (nonatomic, assign) BOOL localAudioEnable; @property (weak, nonatomic) IBOutlet SLSVideoGridView *grid; @property (weak, nonatomic) IBOutlet UILabel *duration; @end @implementation SellyVideoCallConferenceController - (void)viewDidLoad { [super viewDidLoad]; self.title = @"音视频会议"; // Do any additional setup after loading the view. self.session.delegate = self; if (self.videoConfig == nil) { self.videoConfig = SellyRTCVideoConfiguration.defaultConfig; } self.session.videoConfig = self.videoConfig; NSString *token = [TokenGenerator generateTokenWithUserId:SellyRTCEngine.sharedEngine.userId callId:self.channelId]; [self.session startWithChannelId:self.channelId token:token]; //开启视频 [self onCameraClick:nil]; //设置扬声器播放 { //使用这种方案,通话接通前无法在receiver和speaker直接来回切换,强制扬声器 // [self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker]; } { //使用这种方案,通话接通前可以在receiver和speaker直接来回切换 NSError *error; [AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionDuckOthers|AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionMixWithOthers error:&error]; [self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker]; } //显示本地view [self attachLocalStream]; self.localAudioEnable = true; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [self.session end]; } - (IBAction)onSpeakerClick:(id)sender { //如果没有外接设备,可以直接调用这个方法在听筒和扬声器直接来回切换 AVAudioSessionPort currentPort = AVAudioSession.sharedInstance.currentRoute.outputs.firstObject.portType; if ([currentPort isEqualToString:AVAudioSessionPortBuiltInSpeaker]) { [self.session setAudioOutput:AVAudioSessionPortOverrideNone]; } else { [self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker]; } } - (IBAction)onCameraClick:(id)sender { [self.session enableLocalVideo:!self.localVideoEnable]; self.localVideoEnable = !self.localVideoEnable; } - (IBAction)onSwitchClick:(id)sender { [self.session switchCamera]; } - (IBAction)onMuteClick:(id)sender { [self.session enableLocalAudio:!self.localAudioEnable]; self.localAudioEnable = !self.localAudioEnable; } - (void)attachLocalStream { NSString *uid = SellyRTCEngine.sharedEngine.userId; // 1. Grid 布局请求一个格子 SLSVideoTileView *container = [self.grid ensureRenderContainerForUID:uid displayName:uid]; // 2. 将本地视频绑定到 container SellyRtcVideoCanvas *canvas = [[SellyRtcVideoCanvas alloc] init]; // 你的接口要求 userId,这里用字符串版本地 uid canvas.userId = uid; canvas.view = container.contentView; [self.session setLocalCanvas:canvas]; } #pragma marks SLSVideoEngineEvents #pragma mark - SellyRTCSessionDelegate 回调桥接到 SLSVideoEngineEvents /// 无法恢复的错误 - (void)rtcSession:(SellyRTCSession *)session onError:(NSError *)error { // 这里看你要不要额外给业务一个错误回调,可以扩展 SLSVideoEngineEvents NSLog(@"rtc.onerror == %@",error); [self.view showToast:error.localizedDescription]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.navigationController popViewControllerAnimated:true]; }); } /// 远端用户加入 - (void)rtcSession:(SellyRTCSession *)session onUserJoined:(NSString *)userId { SLSVideoTileView *container = [self.grid ensureRenderContainerForUID:userId displayName:userId]; SellyRtcVideoCanvas *canvas = [[SellyRtcVideoCanvas alloc] init]; canvas.userId = userId; canvas.view = container.contentView; [self.session setRemoteCanvas:canvas]; } /// 远端用户离开 - (void)rtcSession:(SellyRTCSession *)session onUserLeave:(NSString *)userId { [self.grid detachUID:userId]; } - (void)rtcSession:(SellyRTCSession *)session audioEnabled:(BOOL)enabled userId:(NSString *)userId { NSLog(@"userId == %@ audioEnabled == %d",userId,enabled); } - (void)rtcSession:(SellyRTCSession *)session videoEnabled:(BOOL)enabled userId:(NSString *)userId { NSLog(@"userId == %@ videoEnabled == %d",userId,enabled); } /// 收到自定义消息 - (void)rtcSession:(SellyRTCSession *)session didReceiveMessage:(NSString *)message userId:(NSString *)userId { NSLog(@"recv message from %@: %@", userId, message); } /// 连接状态变化 - (void)rtcSession:(SellyRTCSession *)session connectionStateChanged:(SellyRTCConnectState)state userId:(nullable NSString *)userId { NSLog(@"ice.connectionStateChanged == %ld",state); } - (void)rtcSession:(SellyRTCSession *)session onRoomConnectionStateChanged:(SellyRoomConnectionState)state { NSLog(@"####onSocketStateChanged == %ld",(long)state); } /// 视频前处理(如果你要做美颜,可以在这里转发给业务;目前和 UI 层无关) - (CVPixelBufferRef)rtcSession:(SellyRTCSession *)session onCaptureVideoFrame:(CVPixelBufferRef)pixelBuffer { // 默认不处理,直接返回原始帧 return pixelBuffer; } /// 统计信息(如果你的 SellyRTCP2pStats 里有音量相关信息,可以在这里转成 videoEngineVolumeIndication) - (void)rtcSession:(SellyRTCSession *)session onStats:(SellyRTCP2pStats *)stats userId:(nullable NSString *)userId { // TODO: 如果 stats 里有本地 / 远端音量,可在这里构造一个 dict 调用: // if ([self.eventsDelegate respondsToSelector:@selector(videoEngineVolumeIndication:)]) { ... } SLSVideoTileView *view = [self.grid ensureRenderContainerForUID:userId displayName:nil]; view.stats = stats; } - (void)rtcSession:(SellyRTCSession *)session onDuration:(NSInteger)duration { self.duration.text = [NSString stringWithFormat:@"%02ld:%02ld",duration/60,duration%60]; } - (void)rtcSession:(SellyRTCSession *)session tokenWillExpire:(NSString *)token { NSString *newToken = [TokenGenerator generateTokenWithUserId:SellyRTCEngine.sharedEngine.userId callId:self.channelId]; [session renewToken:newToken]; } - (void)rtcSession:(SellyRTCSession *)session tokenExpired:(NSString *)token { NSString *newToken = [TokenGenerator generateTokenWithUserId:SellyRTCEngine.sharedEngine.userId callId:self.channelId]; [session renewToken:newToken]; } - (SellyRTCSession *)session { if (!_session) { _session = [[SellyRTCSession alloc] initWithType:false]; } return _session; } /* #pragma mark - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { // Get the new view controller using [segue destinationViewController]. // Pass the selected object to the new view controller. } */ @end