initial commit

This commit is contained in:
Caleb
2026-03-01 15:59:27 +08:00
commit a9e97d56cb
1426 changed files with 172367 additions and 0 deletions

View 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];
//
{
//使receiverspeaker
// [self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker];
}
{
//使receiverspeaker
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