Files
SellyCloudSDK_demo/Example/SellyCloudSDK/VideoCall/SellyVideoCallViewController.m
2026-03-01 15:59:27 +08:00

384 lines
14 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// SellyVideoCallViewController.m
// SellyCloudSDK_Example
//
// Created by Caleb on 3/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "SellyVideoCallViewController.h"
#import <SellyCloudSDK/SellyCloudManager.h>
#import "FUManager.h"
#import "UIView+SellyCloud.h"
#import "SellyCallPiPManager.h"
#import "TokenGenerator.h"
#import "SellyCallControlView.h"
#import "SellyCallStatsView.h"
#import <Masonry/Masonry.h>
@interface SellyVideoCallViewController ()<SellyRTCSessionDelegate, SellyCallControlViewDelegate>
@property (weak, nonatomic) IBOutlet UIView *localView;
@property (weak, nonatomic) IBOutlet UIView *remoteView;
@property (nonatomic, strong)SellyRTCSession *session;
@property (nonatomic, assign)BOOL localVideoEnable;
@property (nonatomic, assign)BOOL localAudioEnable;
@property (nonatomic, assign)BOOL speakerEnabled;
@property (nonatomic, strong)AVAudioPlayer *player;
@property (nonatomic, strong) SellyCallPiPManager *pipManager;
// 底部控制视图
@property (nonatomic, strong) SellyCallControlView *controlView;
// 统计信息视图
@property (nonatomic, strong) SellyCallStatsView *statsView;
@property (nonatomic, assign) BOOL remoteUserJoined; // 标记远端用户是否加入
@end
@implementation SellyVideoCallViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"音视频单聊";
// 初始化状态
self.remoteUserJoined = NO;
self.speakerEnabled = YES; // 默认扬声器开启
self.localAudioEnable = YES; // 默认麦克风开启
// 隐藏远端视图,等对方加入后再显示
self.remoteView.hidden = YES;
// 设置底部控制视图
[self setupControlView];
// 设置统计信息视图
[self setupStatsView];
// Do any additional setup after loading the view from its nib.
SellyRTCVideoCanvas *localCanvas = SellyRTCVideoCanvas.new;
localCanvas.view = self.localView;
localCanvas.userId = SellyCloudManager.sharedInstance.userId;
[self.session setLocalCanvas:localCanvas];
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];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//模拟3s后接通
NSString *token = [TokenGenerator generateRTCCallTokenWithUserId:SellyCloudManager.sharedInstance.userId callId:self.channelId];
[self.session startWithChannelId:self.channelId token:token];
});
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// 隐藏导航栏
[self.navigationController setNavigationBarHidden:YES animated:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 恢复导航栏
[self.navigationController setNavigationBarHidden:NO animated:animated];
}
- (void)setupControlView {
// 创建底部控制视图
self.controlView = [[SellyCallControlView alloc] init];
self.controlView.delegate = self;
// 单聊场景显示画中画按钮
self.controlView.showPiPButton = YES;
[self.view addSubview:self.controlView];
// 使用 Masonry 设置约束
[self.controlView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.view);
make.bottom.equalTo(self.view);
make.height.mas_equalTo(210); // 调整高度以适应两行按钮
}];
// 初始化按钮状态
[self.controlView updateSpeakerEnabled:self.speakerEnabled];
[self.controlView updateVideoEnabled:self.localVideoEnable];
[self.controlView updateMuteEnabled:!self.localAudioEnable];
[self.controlView updatePiPEnabled:NO]; // 初始画中画未激活
}
- (void)setupStatsView {
// 创建统计信息视图
self.statsView = [[SellyCallStatsView alloc] init];
self.statsView.hidden = YES; // 初始隐藏,通话接通后显示
[self.view addSubview:self.statsView];
// 使用 Masonry 设置约束 - 放在左上角
[self.statsView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.view).offset(16);
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(16);
make.width.mas_equalTo(200);
make.height.mas_equalTo(150);
}];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.session end];
[self QCM_stopRing];
//正常情况下退出通话要保持,这里只做演示
[self.pipManager invalidate];
self.pipManager = nil;
}
- (IBAction)onSpeakerClick:(id)sender {
//如果没有外接设备,可以直接调用这个方法在听筒和扬声器直接来回切换
AVAudioSessionPort currentPort = AVAudioSession.sharedInstance.currentRoute.outputs.firstObject.portType;
if ([currentPort isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
[self.session setAudioOutput:AVAudioSessionPortOverrideNone];
self.speakerEnabled = NO;
}
else {
[self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker];
self.speakerEnabled = YES;
}
[self.controlView updateSpeakerEnabled:self.speakerEnabled];
}
- (IBAction)onCameraClick:(id)sender {
[self.session enableLocalVideo:!self.localVideoEnable];
self.localVideoEnable = !self.localVideoEnable;
[self.controlView updateVideoEnabled:self.localVideoEnable];
}
- (IBAction)onSwitchClick:(id)sender {
[self.session switchCamera];
}
- (IBAction)onMuteClick:(id)sender {
[self.session enableLocalAudio:!self.localAudioEnable];
self.localAudioEnable = !self.localAudioEnable;
[self.controlView updateMuteEnabled:!self.localAudioEnable];
}
- (IBAction)onHangupClick:(id)sender {
[self.navigationController popViewControllerAnimated:YES];
}
- (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 继续默认渲染到你设置的 canvaslocalView/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:(SellyRTCStats *)stats userId:(nullable NSString *)userId {
NSString *bitrate = [NSString stringWithFormat:@"%ld/%ld kbps", (NSInteger)stats.txKbps, (NSInteger)stats.rxKbps];
NSString *fps = [NSString stringWithFormat:@"%ld/%ld fps", (NSInteger)stats.sentFps, (NSInteger)stats.recvFps];
NSString *rtt = [NSString stringWithFormat:@"%ld ms", (NSInteger)stats.transportRttMs];
NSString *codec = [NSString stringWithFormat:@"%@/%@", stats.videoCodec, stats.audioCodec];
NSString *videoSize = [NSString stringWithFormat:@"%ldx%ld", stats.recvWidth, stats.recvHeight];
[self.statsView updateBitrate:bitrate];
[self.statsView updateVideoFps:fps];
[self.statsView updateRtt:rtt];
[self.statsView updateCodec:codec];
[self.statsView updateVideoSize:videoSize];
}
- (void)rtcSession:(SellyRTCSession *)session onDuration:(NSInteger)duration {
NSString *durationStr = [NSString stringWithFormat:@"%02ld:%02ld", duration/60, duration%60];
[self.statsView updateDuration:durationStr];
}
- (void)rtcSession:(SellyRTCSession *)session onUserJoined:(NSString *)userId {
NSLog(@"###onUserJoined == %@",userId);
// 标记远端用户已加入
self.remoteUserJoined = YES;
// 显示远端视图
self.remoteView.hidden = NO;
SellyRTCVideoCanvas *remoteCanvas = SellyRTCVideoCanvas.new;
remoteCanvas.view = self.remoteView;
remoteCanvas.userId = userId;
[self.session setRemoteCanvas:remoteCanvas];
[self QCM_stopRing];
// 通话接通后显示统计信息
[self.statsView show];
}
- (void)rtcSession:(SellyRTCSession *)session onUserLeave:(NSString *)userId {
NSLog(@"####onUserLeave == %@",userId);
// 隐藏远端视图
self.remoteView.hidden = YES;
self.remoteUserJoined = NO;
// 隐藏统计信息
[self.statsView hide];
[self.navigationController popViewControllerAnimated:true];
}
- (void)rtcSession:(SellyRTCSession *)session tokenWillExpire:(NSString *)token {
NSString *newToken = [TokenGenerator generateRTCCallTokenWithUserId:SellyCloudManager.sharedInstance.userId callId:self.channelId];
[session renewToken:newToken];
}
- (void)rtcSession:(SellyRTCSession *)session tokenExpired:(NSString *)token {
NSString *newToken = [TokenGenerator generateRTCCallTokenWithUserId:SellyCloudManager.sharedInstance.userId callId:self.channelId];
[session renewToken:newToken];
}
- (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];
}
#pragma mark - SellyCallControlViewDelegate
- (void)callControlView:(SellyCallControlView *)controlView didTapAction:(SellyCallControlAction)action {
switch (action) {
case SellyCallControlActionSpeaker:
[self onSpeakerClick:nil];
break;
case SellyCallControlActionVideo:
[self onCameraClick:nil];
break;
case SellyCallControlActionSwitchCamera:
[self onSwitchClick:nil];
break;
case SellyCallControlActionMute:
[self onMuteClick:nil];
break;
case SellyCallControlActionPiP:
[self onActionPIP:nil];
// 更新画中画按钮状态
if (@available(iOS 15.0, *)) {
[self.controlView updatePiPEnabled:self.pipManager.pipActive];
}
break;
case SellyCallControlActionHangup:
[self onHangupClick:nil];
break;
default:
break;
}
}
@end