258 lines
9.7 KiB
Objective-C
258 lines
9.7 KiB
Objective-C
//
|
||
// 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"
|
||
|
||
@interface SellyVideoCallViewController ()<SellyRTCSessionDelegate>
|
||
@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
|