initial commit
This commit is contained in:
383
Example/SellyCloudSDK/VideoCall/SellyVideoCallViewController.m
Normal file
383
Example/SellyCloudSDK/VideoCall/SellyVideoCallViewController.m
Normal file
@@ -0,0 +1,383 @@
|
||||
//
|
||||
// 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 继续默认渲染到你设置的 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:(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
|
||||
Reference in New Issue
Block a user