// // SellyVideoCallViewController.m // SellyCloudSDK_Example // // Created by Caleb on 3/11/25. // Copyright © 2025 Caleb. All rights reserved. // #import "SellyVideoCallViewController.h" #import #import "FUManager.h" #import "UIView+SellyCloud.h" #import "SellyCallPiPManager.h" #import "TokenGenerator.h" @interface SellyVideoCallViewController () @property (weak, nonatomic) IBOutlet UIView *localView; @property (weak, nonatomic) IBOutlet UIView *remoteView; @property (nonatomic, strong)SellyRTCEngine *engine; @property (nonatomic, strong)SellyRTCSession *session; @property (nonatomic, assign)BOOL localVideoEnable; @property (nonatomic, assign)BOOL localAudioEnable; @property (nonatomic, strong)AVAudioPlayer *player; @property (weak, nonatomic) IBOutlet UILabel *bitrate; @property (weak, nonatomic) IBOutlet UILabel *videoFps; @property (weak, nonatomic) IBOutlet UILabel *rtt; @property (weak, nonatomic) IBOutlet UILabel *codec; @property (weak, nonatomic) IBOutlet UILabel *duration; @property (weak, nonatomic) IBOutlet UILabel *videoSize; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *remoteWConstraint; @property (nonatomic, strong) SellyCallPiPManager *pipManager; @end @implementation SellyVideoCallViewController - (void)viewDidLoad { [super viewDidLoad]; self.title = @"音视频单聊"; // Do any additional setup after loading the view from its nib. SellyRtcVideoCanvas *localCanvas = SellyRtcVideoCanvas.new; localCanvas.view = self.localView; localCanvas.userId = SellyRTCEngine.sharedEngine.userId; [self.session setLocalCanvas:localCanvas]; self.session.delegate = self; self.session.delegate = self; if (self.videoConfig == nil) { self.videoConfig = SellyRTCVideoConfiguration.defaultConfig; } self.session.videoConfig = self.videoConfig; //模拟先本地预览响铃,5s后接通的流程 [self.session startPreview]; //开启视频 [self onCameraClick:nil]; //模拟等待接听铃声 [self playSourceName:nil numberOfLoops:100]; //设置扬声器播放 { //使用这种方案,通话接通前无法在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]; } self.remoteWConstraint.constant = 200*self.session.videoConfig.resolution.height/self.session.videoConfig.resolution.width; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //模拟3s后接通 NSString *token = [TokenGenerator generateTokenWithUserId:SellyRTCEngine.sharedEngine.userId callId:self.channelId]; [self.session startWithChannelId:self.channelId token:token]; }); //默认语音开启 self.localAudioEnable = true; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [self.session end]; [self QCM_stopRing]; //正常情况下退出通话要保持,这里只做演示 if (@available(iOS 15.0, *)) { if (self.pipManager.pipActive) { [self.pipManager togglePiP]; // 关掉 PiP } } } - (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; } - (IBAction)onActionPIP:(id)sender { if (@available(iOS 15.0, *)) { if (self.pipManager.pipPossible) { [self.pipManager togglePiP]; } else { [self.view showToast:@"当前设备不支持画中画"]; } } else { [self.view showToast:@"iOS 15 以上才支持自定义 PiP"]; } } - (void)playSourceName:(NSString *)source numberOfLoops:(NSInteger)numberOfLoops { NSString *url = [NSBundle.mainBundle pathForResource:@"call" ofType:@"caf"]; _player = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:url] error:nil]; _player.numberOfLoops = numberOfLoops; [_player play]; } - (void)QCM_stopRing { if (_player && _player.isPlaying) { [_player stop]; } } #pragma marks SellyRTCSessionDelegate - (void)rtcSession:(SellyRTCSession *)session didReceiveMessage:(NSString *)message userId:(NSString *)userId { NSLog(@"userId == %@ didReceiveMessage == %@",userId,message); } - (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 connectionStateChanged:(SellyRTCConnectState)state userId:(nullable NSString *)userId { NSLog(@"ice.connectionStateChanged == %ld",state); // 初始化 PiP Manager if (state == SellyRTCConnectStateConnected && !self.pipManager) { self.pipManager = [[SellyCallPiPManager alloc] initWithRenderView:self.remoteView]; [self.pipManager setupIfNeeded]; } } - (void)rtcSession:(SellyRTCSession *)session onRoomConnectionStateChanged:(SellyRoomConnectionState)state { NSLog(@"onSocketStateChanged == %ld",(long)state); } - (CVPixelBufferRef)rtcSession:(SellyRTCSession *)session onCaptureVideoFrame:(CVPixelBufferRef)pixelBuffer { CVPixelBufferRef afterBuffer = [FUManager.shareManager renderItemsToPixelBuffer:pixelBuffer]; return afterBuffer; } //返回false sdk内部将不会默认渲染改帧 - (BOOL)rtcSession:(SellyRTCSession *)session onRenderVideoFrame:(SellyRTCVideoFrame *)videoFrame userId:(NSString *)userId { // 1. SDK 继续默认渲染到你设置的 canvas(localView/remoteView) // 2. 同时,我们把远端的帧喂给 PiP layer。假设你想 PiP 显示远端 userId: [self.pipManager feedVideoFrame:videoFrame]; return false; // 仍然让 SDK 内部按 canvas 渲染 } - (void)rtcSession:(SellyRTCSession *)session onError:(NSError *)error { NSLog(@"rtcSession.error == %@",error); [self.session end]; [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 onStats:(SellyRTCP2pStats *)stats userId:(nullable NSString *)userId { self.bitrate.text = [NSString stringWithFormat:@"%ld/%ld",(NSInteger)stats.txKbps,(NSInteger)stats.rxKbps]; self.videoFps.text = [NSString stringWithFormat:@"%ld/%ld",(NSInteger)stats.sentFps,(NSInteger)stats.recvFps]; self.rtt.text = [NSString stringWithFormat:@"%ld",(NSInteger)stats.transportRttMs]; self.codec.text = [NSString stringWithFormat:@"%@/%@",stats.videoCodec,stats.audioCodec]; self.videoSize.text = [NSString stringWithFormat:@"%ldx%ld",stats.recvWidth,stats.recvHeight]; } - (void)rtcSession:(SellyRTCSession *)session onDuration:(NSInteger)duration { self.duration.text = [NSString stringWithFormat:@"%02ld:%02ld",duration/60,duration%60]; } - (void)rtcSession:(SellyRTCSession *)session onUserJoined:(NSString *)userId { NSLog(@"###onUserJoined == %@",userId); SellyRtcVideoCanvas *remoteCanvas = SellyRtcVideoCanvas.new; remoteCanvas.view = self.remoteView; remoteCanvas.userId = userId; [self.session setRemoteCanvas:remoteCanvas]; [self QCM_stopRing]; } - (void)rtcSession:(SellyRTCSession *)session onUserLeave:(NSString *)userId { NSLog(@"####onUserLeave == %@",userId); [self.navigationController popViewControllerAnimated:true]; } - (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]; } - (SellyRTCEngine *)engine { return SellyRTCEngine.sharedEngine; } - (SellyRTCSession *)session { if (!_session) { _session = [[SellyRTCSession alloc] initWithType:true]; } return _session; } - (void)dealloc { [self.session setAudioOutput:AVAudioSessionPortOverrideNone]; NSError *error; [AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayback error:&error]; } @end