243 lines
9.0 KiB
Objective-C
243 lines
9.0 KiB
Objective-C
//
|
||
// SellyVideoCallConferenceController.m
|
||
// SellyCloudSDK_Example
|
||
//
|
||
// Created by Caleb on 11/11/25.
|
||
// Copyright © 2025 Caleb. All rights reserved.
|
||
//
|
||
|
||
#import "SellyVideoCallConferenceController.h"
|
||
#import <SellyCloudSDK/SellyCloudManager.h>
|
||
#import "FUManager.h"
|
||
#import "UIView+SellyCloud.h"
|
||
#import "SLSVideoGridView.h"
|
||
#import "TokenGenerator.h"
|
||
#import <ReplayKit/ReplayKit.h> //屏幕采集
|
||
|
||
@interface SellyVideoCallConferenceController ()<SellyRTCSessionDelegate>
|
||
|
||
// 你的 WebRTC 会话对象
|
||
@property (nonatomic, strong) SellyRTCSession *session;
|
||
|
||
@property (nonatomic, assign) BOOL localVideoEnable;
|
||
@property (nonatomic, assign) BOOL localAudioEnable;
|
||
|
||
@property (weak, nonatomic) IBOutlet SLSVideoGridView *grid;
|
||
@property (weak, nonatomic) IBOutlet UILabel *duration;
|
||
@property (nonatomic, strong)RPSystemBroadcastPickerView *systemBroadcastPicker;
|
||
@end
|
||
|
||
@implementation SellyVideoCallConferenceController
|
||
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
self.title = @"音视频会议";
|
||
// Do any additional setup after loading the view.
|
||
|
||
self.session.delegate = self;
|
||
if (self.videoConfig == nil) {
|
||
self.videoConfig = SellyRTCVideoConfiguration.defaultConfig;
|
||
}
|
||
self.session.videoConfig = self.videoConfig;
|
||
|
||
NSString *token = [TokenGenerator generateTokenWithUserId:SellyRTCEngine.sharedEngine.userId callId:self.channelId];
|
||
[self.session startWithChannelId:self.channelId token:token];
|
||
|
||
//开启视频
|
||
[self onCameraClick:nil];
|
||
|
||
//设置扬声器播放
|
||
{
|
||
//使用这种方案,通话接通前无法在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];
|
||
}
|
||
//显示本地view
|
||
[self attachLocalStream];
|
||
self.localAudioEnable = true;
|
||
|
||
[self addBroadcastButton];
|
||
|
||
}
|
||
|
||
- (void)addBroadcastButton {
|
||
if (@available(iOS 12.0, *)) {
|
||
self.systemBroadcastPicker = [[RPSystemBroadcastPickerView alloc]
|
||
initWithFrame:CGRectMake(UIScreen.mainScreen.bounds.size.width-80, UIScreen.mainScreen.bounds.size.height-180, 60, 60)];
|
||
|
||
NSString *bundleId = [NSBundle mainBundle].bundleIdentifier;
|
||
self.systemBroadcastPicker.preferredExtension = [NSString stringWithFormat:@"%@.ScreenShareUploader",bundleId];// 你的 extension bundle id
|
||
self.systemBroadcastPicker.showsMicrophoneButton = false;
|
||
}
|
||
}
|
||
|
||
- (IBAction)startScreenCapture:(id)sender {
|
||
for (UIView *view in self.systemBroadcastPicker.subviews) {
|
||
if ([view isKindOfClass:[UIButton class]]) {
|
||
[((UIButton *)view) sendActionsForControlEvents:(UIControlEventAllEvents)];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)viewDidDisappear:(BOOL)animated {
|
||
[super viewDidDisappear:animated];
|
||
[self.session end];
|
||
}
|
||
|
||
- (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;
|
||
}
|
||
|
||
|
||
- (void)attachLocalStream {
|
||
NSString *uid = SellyRTCEngine.sharedEngine.userId;
|
||
|
||
// 1. Grid 布局请求一个格子
|
||
SLSVideoTileView *container = [self.grid ensureRenderContainerForUID:uid
|
||
displayName:uid];
|
||
|
||
// 2. 将本地视频绑定到 container
|
||
SellyRtcVideoCanvas *canvas = [[SellyRtcVideoCanvas alloc] init];
|
||
// 你的接口要求 userId,这里用字符串版本地 uid
|
||
canvas.userId = uid;
|
||
canvas.view = container.contentView;
|
||
[self.session setLocalCanvas:canvas];
|
||
}
|
||
|
||
#pragma marks SLSVideoEngineEvents
|
||
|
||
#pragma mark - SellyRTCSessionDelegate 回调桥接到 SLSVideoEngineEvents
|
||
|
||
/// 无法恢复的错误
|
||
- (void)rtcSession:(SellyRTCSession *)session onError:(NSError *)error {
|
||
// 这里看你要不要额外给业务一个错误回调,可以扩展 SLSVideoEngineEvents
|
||
NSLog(@"rtc.onerror == %@",error);
|
||
[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 onUserJoined:(NSString *)userId {
|
||
SLSVideoTileView *container = [self.grid ensureRenderContainerForUID:userId
|
||
displayName:userId];
|
||
SellyRtcVideoCanvas *canvas = [[SellyRtcVideoCanvas alloc] init];
|
||
canvas.userId = userId;
|
||
canvas.view = container.contentView;
|
||
[self.session setRemoteCanvas:canvas];
|
||
}
|
||
|
||
/// 远端用户离开
|
||
- (void)rtcSession:(SellyRTCSession *)session onUserLeave:(NSString *)userId {
|
||
[self.grid detachUID:userId];
|
||
}
|
||
|
||
- (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 didReceiveMessage:(NSString *)message userId:(NSString *)userId {
|
||
NSLog(@"recv message from %@: %@", userId, message);
|
||
}
|
||
|
||
/// 连接状态变化
|
||
- (void)rtcSession:(SellyRTCSession *)session connectionStateChanged:(SellyRTCConnectState)state userId:(nullable NSString *)userId {
|
||
NSLog(@"ice.connectionStateChanged == %ld",state);
|
||
}
|
||
|
||
- (void)rtcSession:(SellyRTCSession *)session onRoomConnectionStateChanged:(SellyRoomConnectionState)state {
|
||
NSLog(@"####onSocketStateChanged == %ld",(long)state);
|
||
}
|
||
|
||
/// 视频前处理(如果你要做美颜,可以在这里转发给业务;目前和 UI 层无关)
|
||
- (CVPixelBufferRef)rtcSession:(SellyRTCSession *)session onCaptureVideoFrame:(CVPixelBufferRef)pixelBuffer {
|
||
// 默认不处理,直接返回原始帧
|
||
return pixelBuffer;
|
||
}
|
||
|
||
/// 统计信息(如果你的 SellyRTCP2pStats 里有音量相关信息,可以在这里转成 videoEngineVolumeIndication)
|
||
- (void)rtcSession:(SellyRTCSession *)session onStats:(SellyRTCP2pStats *)stats userId:(nullable NSString *)userId {
|
||
// TODO: 如果 stats 里有本地 / 远端音量,可在这里构造一个 dict 调用:
|
||
// if ([self.eventsDelegate respondsToSelector:@selector(videoEngineVolumeIndication:)]) { ... }
|
||
SLSVideoTileView *view = [self.grid ensureRenderContainerForUID:userId displayName:nil];
|
||
view.stats = stats;
|
||
}
|
||
|
||
- (void)rtcSession:(SellyRTCSession *)session onDuration:(NSInteger)duration {
|
||
self.duration.text = [NSString stringWithFormat:@"%02ld:%02ld",duration/60,duration%60];
|
||
}
|
||
|
||
- (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];
|
||
}
|
||
|
||
- (void)rtcSession:(SellyRTCSession *)session onScreenShareStatusChanged:(SellyScreenShareState)state {
|
||
if (state == SellyScreenShareStateStarted) {
|
||
self.localVideoEnable = false;
|
||
[self.session startScreenCapture];
|
||
}
|
||
else if (state == SellyScreenShareStateStopped) {
|
||
self.localVideoEnable = true;
|
||
[self.session enableLocalVideo:true];
|
||
}
|
||
}
|
||
|
||
- (SellyRTCSession *)session {
|
||
if (!_session) {
|
||
_session = [[SellyRTCSession alloc] initWithType:false];
|
||
}
|
||
return _session;
|
||
}
|
||
|
||
/*
|
||
#pragma mark - Navigation
|
||
|
||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||
// Get the new view controller using [segue destinationViewController].
|
||
// Pass the selected object to the new view controller.
|
||
}
|
||
*/
|
||
|
||
@end
|