This commit is contained in:
Caleb
2025-11-21 12:00:40 +08:00
commit 18ea900c12
1189 changed files with 134605 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
//
// SLSParticipant.h
// SellyCloudSDK_Example
//
// Created by Caleb on 13/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SLSParticipant : NSObject
@property (nonatomic, copy) NSString *uid;
@property (nonatomic, copy) NSString *displayName;
/// 音量 0~1用来做说话者高亮
@property (nonatomic, assign) CGFloat level;
/// 音频 / 视频 是否静音
@property (nonatomic, assign) BOOL audioMuted;
@property (nonatomic, assign) BOOL videoMuted;
/// 上次活跃时间(可选,用于后面做发言排序等)
@property (nonatomic, strong) NSDate *lastActiveAt;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,13 @@
//
// SLSParticipant.m
// SellyCloudSDK_Example
//
// Created by Caleb on 13/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "SLSParticipant.h"
@implementation SLSParticipant
@end

View File

@@ -0,0 +1,38 @@
//
// SLSVideoGridView.h
// SellyCloudSDK_Example
//
// Created by Caleb on 13/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SLSParticipant.h"
#import "SLSVideoTileView.h"
NS_ASSUME_NONNULL_BEGIN
@interface SLSVideoGridView : UIView
@property (nonatomic, assign) CGFloat spacing; // 默认 8
@property (nonatomic, assign) UIEdgeInsets padding;// 默认 {12,12,12,12}
@property (nonatomic, assign) BOOL keepAspect169; // 默认 YES
/// 创建或获取某个 uid 的 tile并返回其中的 contentView 作为渲染容器
- (SLSVideoTileView *)ensureRenderContainerForUID:(NSString *)uid
displayName:(nullable NSString *)name;
/// 移除某个 uid用户离开
- (void)detachUID:(NSString *)uid;
/// 更新音量0~1用于说话者高亮
- (void)setLevel:(CGFloat)level forUID:(NSString *)uid;
/// 更新静音状态
- (void)setAudioMuted:(BOOL)muted forUID:(NSString *)uid;
- (void)setVideoMuted:(BOOL)muted forUID:(NSString *)uid;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,180 @@
//
// SLSVideoGridView.m
// SellyCloudSDK_Example
//
// Created by Caleb on 13/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "SLSVideoGridView.h"
@interface SLSVideoGridView ()
@property (nonatomic, strong) NSMutableDictionary<NSString*, SLSParticipant*> *participants;
@property (nonatomic, strong) NSMutableDictionary<NSString*, SLSVideoTileView*> *tiles;
@end
@implementation SLSVideoGridView
//
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
// xib / storyboard
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self commonInit];
}
return self;
}
//
- (void)commonInit {
_participants = [NSMutableDictionary dictionary];
_tiles = [NSMutableDictionary dictionary];
_spacing = 8.f;
_padding = UIEdgeInsetsMake(8, 8, 8, 8);
_keepAspect169 = YES;
self.backgroundColor = [UIColor blackColor];
}
#pragma mark - Public
- (SLSVideoTileView *)ensureRenderContainerForUID:(NSString *)uid
displayName:(nullable NSString *)name {
if (!uid) return nil;
SLSParticipant *p = self.participants[uid];
if (!p) {
p = [SLSParticipant new];
p.uid = uid;
p.displayName = name ?: uid;
p.lastActiveAt = [NSDate date];
self.participants[uid] = p;
}
SLSVideoTileView *tile = self.tiles[uid];
if (!tile) {
tile = [[SLSVideoTileView alloc] initWithFrame:CGRectZero];
self.tiles[uid] = tile;
[self addSubview:tile];
}
[tile updateWithParticipant:p];
[self _layout];
return tile;
}
- (void)detachUID:(NSString *)uid {
if (!uid) return;
SLSVideoTileView *tile = self.tiles[uid];
if (tile) {
[tile removeFromSuperview];
[self.tiles removeObjectForKey:uid];
}
[self.participants removeObjectForKey:uid];
[self _layout];
}
- (void)setLevel:(CGFloat)level forUID:(NSString *)uid {
SLSParticipant *p = self.participants[uid];
if (!p) return;
p.level = MAX(0.f, MIN(level, 1.f));
if (p.level > 0.1) {
p.lastActiveAt = [NSDate date];
}
SLSVideoTileView *tile = self.tiles[uid];
[tile updateWithParticipant:p];
}
- (void)setAudioMuted:(BOOL)muted forUID:(NSString *)uid {
SLSParticipant *p = self.participants[uid];
if (!p) return;
p.audioMuted = muted;
SLSVideoTileView *tile = self.tiles[uid];
[tile updateWithParticipant:p];
}
- (void)setVideoMuted:(BOOL)muted forUID:(NSString *)uid {
SLSParticipant *p = self.participants[uid];
if (!p) return;
p.videoMuted = muted;
SLSVideoTileView *tile = self.tiles[uid];
[tile updateWithParticipant:p];
}
#pragma mark - Layout
- (void)_layout {
NSArray<NSString *> *uids = self.participants.allKeys;
NSInteger n = uids.count;
if (n == 0) return;
UIEdgeInsets p = self.padding;
CGFloat W = self.bounds.size.width - p.left - p.right;
CGFloat H = self.bounds.size.height - p.top - p.bottom;
CGFloat s = self.spacing;
// ===== =====
NSInteger cols = 1;
NSInteger rows = 1;
if (n <= 4) {
cols = 2;
rows = 2;
} else if (n <= 9) {
cols = 3;
rows = 3;
} else if (n <= 16) {
cols = 4;
rows = 4;
} else {
// >16 4x4
cols = 4;
rows = 4;
}
// cell / 1/21/31/4
CGFloat cellW = 0;
CGFloat cellH = 0;
if (cols > 0) {
cellW = floor((W - s * (cols - 1)) / cols);
}
if (rows > 0) {
cellH = floor((H - s * (rows - 1)) / rows);
}
NSInteger maxCells = cols * rows;
NSInteger countToLayout = MIN(n, maxCells);
[uids enumerateObjectsUsingBlock:^(NSString *uid, NSUInteger idx, BOOL *stop) {
if (idx >= countToLayout) {
*stop = YES;
return;
}
NSInteger r = idx / cols; //
NSInteger c = idx % cols; //
CGFloat x = p.left + c * (cellW + s);
CGFloat y = p.top + r * (cellH + s);
SLSVideoTileView *tile = self.tiles[uid];
tile.frame = CGRectMake(x, y, cellW, cellH);
SLSParticipant *part = self.participants[uid];
[tile updateWithParticipant:part];
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self _layout];
}
@end

View File

@@ -0,0 +1,27 @@
//
// SLSVideoTileView.h
// SellyCloudSDK_Example
//
// Created by Caleb on 13/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <SellyCloudSDK/SellyCloudManager.h>
#import "SLSParticipant.h"
NS_ASSUME_NONNULL_BEGIN
@interface SLSVideoTileView : UIView
/// 容器视图SDK 的渲染 view 挂在这里
@property (nonatomic, strong, readonly) UIView *contentView;
/// 根据参会者状态更新 UI昵称、静音、视频开关、高亮等
- (void)updateWithParticipant:(SLSParticipant *)participant;
@property (nonatomic, strong)SellyRTCP2pStats *stats;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,194 @@
//
// SLSVideoTileView.m
// SellyCloudSDK_Example
//
// Created by Caleb on 13/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "SLSVideoTileView.h"
@interface SLSVideoTileView ()
@property (nonatomic, strong)NSString *userId;
@property (nonatomic, strong) UIView *contentViewInternal;
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UIImageView *muteIcon;
@property (nonatomic, strong) UIView *videoMaskView;
@property (nonatomic, strong) UILabel *videoOffLabel;
@property (nonatomic, strong) UILabel *bitrate;
@property (nonatomic, strong) UILabel *fps;
@property (nonatomic, strong) UILabel *codec;
@property (nonatomic, strong) UILabel *rtt;
@end
@implementation SLSVideoTileView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.layer.cornerRadius = 10.f;
self.clipsToBounds = YES;
self.backgroundColor = [UIColor blackColor];
_contentViewInternal = [[UIView alloc] initWithFrame:self.bounds];
_contentViewInternal.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_contentViewInternal.backgroundColor = [UIColor blackColor];
[self addSubview:_contentViewInternal];
_videoMaskView = [[UIView alloc] initWithFrame:self.bounds];
_videoMaskView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_videoMaskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
_videoMaskView.hidden = YES;
[self addSubview:_videoMaskView];
_videoOffLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_videoOffLabel.textAlignment = NSTextAlignmentCenter;
_videoOffLabel.textColor = [UIColor lightGrayColor];
_videoOffLabel.font = [UIFont systemFontOfSize:14];
_videoOffLabel.text = @"视频已关闭";
[_videoMaskView addSubview:_videoOffLabel];
_nameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_nameLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightMedium];
_nameLabel.textColor = [UIColor whiteColor];
_nameLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
_nameLabel.layer.cornerRadius = 8;
_nameLabel.clipsToBounds = YES;
[self addSubview:_nameLabel];
_muteIcon = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"mic.slash.fill"]];
_muteIcon.tintColor = [UIColor systemRedColor];
[self addSubview:_muteIcon];
[self addSubview:self.bitrate];
[self addSubview:self.fps];
[self addSubview:self.codec];
[self addSubview:self.rtt];
[self.bitrate mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.offset(10);
}];
[self.fps mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.bitrate.mas_bottom).offset(10);
make.left.offset(10);
}];
[self.codec mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.fps.mas_bottom).offset(10);
make.left.offset(10);
}];
[self.rtt mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.codec.mas_bottom).offset(10);
make.left.offset(10);
}];
}
return self;
}
- (UIView *)contentView {
return self.contentViewInternal;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.contentViewInternal.frame = self.bounds;
self.videoMaskView.frame = self.bounds;
self.videoOffLabel.frame = CGRectInset(self.videoMaskView.bounds, 8, 8);
CGFloat padding = 6.f;
CGSize maxSize = CGSizeMake(self.bounds.size.width - 2*padding, 20);
CGSize nameSize = [self.nameLabel sizeThatFits:maxSize];
self.nameLabel.frame = CGRectMake(padding,
self.bounds.size.height - nameSize.height - padding,
nameSize.width + 12,
nameSize.height);
self.muteIcon.frame = CGRectMake(CGRectGetMaxX(self.nameLabel.frame) + 4,
CGRectGetMinY(self.nameLabel.frame) - 2,
18,
18);
}
- (void)updateWithParticipant:(SLSParticipant *)p {
self.userId = p.uid;
//
self.nameLabel.text = p.displayName.length ? p.displayName : p.uid;
//
self.muteIcon.hidden = !p.audioMuted;
// contentView
BOOL videoOn = !p.videoMuted;
self.videoMaskView.hidden = videoOn;
//
if (p.level > 0.25 && !p.audioMuted) {
self.layer.borderWidth = 2.f;
self.layer.borderColor = [UIColor systemGreenColor].CGColor;
} else {
self.layer.borderWidth = 0.f;
self.layer.borderColor = nil;
}
[self setNeedsLayout];
}
- (void)setStats:(SellyRTCP2pStats *)stats {
_stats = stats;
self.rtt.text = [NSString stringWithFormat:@" rtt%ldms ",(NSInteger)stats.transportRttMs];
self.codec.text = [NSString stringWithFormat:@" %@/%@ ",stats.videoCodec,stats.audioCodec];
if ([self.userId isEqualToString:SellyRTCEngine.sharedEngine.userId]) {
self.bitrate.text = [NSString stringWithFormat:@" 码率:%ld kbps ",(NSInteger)(stats.txKbps)];
self.fps.text = [NSString stringWithFormat:@" 视频:%ld fps %ldx%ld ",(NSInteger)(stats.sentFps),(NSInteger)(stats.sentWidth),(NSInteger)(stats.sentHeight)];
}
else {
self.bitrate.text = [NSString stringWithFormat:@" 码率:%ld kbps ",(NSInteger)(stats.rxKbps)];
self.fps.text = [NSString stringWithFormat:@" 视频:%ld fps %ldx%ld ",(NSInteger)(stats.recvFps),(NSInteger)(stats.recvWidth),(NSInteger)(stats.recvHeight)];
}
}
- (UILabel *)bitrate {
if (!_bitrate) {
_bitrate = [self createStatsLabel];
}
return _bitrate;
}
- (UILabel *)fps {
if (!_fps) {
_fps = [self createStatsLabel];
}
return _fps;
}
- (UILabel *)codec {
if (!_codec) {
_codec = [self createStatsLabel];
}
return _codec;
}
- (UILabel *)rtt {
if (!_rtt) {
_rtt = [self createStatsLabel];
}
return _rtt;
}
- (UILabel *)createStatsLabel {
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:12];
label.textColor = [UIColor whiteColor];
label.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2];
label.textAlignment = NSTextAlignmentLeft;
label.layer.cornerRadius = 4;
label.layer.masksToBounds = YES;
return label;
}
@end

View File

@@ -0,0 +1,42 @@
//
// SellyCallPiPManager.h
// SellyCloudSDK_Example
//
// Created by Caleb on 19/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AVKit/AVKit.h>
#import <SellyCloudSDK/SellyCloudManager.h>
NS_ASSUME_NONNULL_BEGIN
/// 负责:
/// - 初始化 AVSampleBufferDisplayLayer
/// - 管理 AVPictureInPictureController
/// - 接收业务层传入的 SellyRTCVideoFrame 并送入 PiP
@interface SellyCallPiPManager : NSObject
<AVPictureInPictureControllerDelegate, AVPictureInPictureSampleBufferPlaybackDelegate>
/// 是否当前设备/系统支持自定义 PiP
@property (nonatomic, assign, readonly) BOOL pipPossible;
/// 方便外界知道当前是否在 PiP
@property (nonatomic, assign, readonly) BOOL pipActive;
/// 指定在哪个 view 里显示App 内)这条 PiP 的画面(可选)
- (instancetype)initWithRenderView:(UIView *)renderView;
/// 初始化 / 配置 PiP通常在 viewDidLoad 调一次
- (void)setupIfNeeded;
/// 外界点按钮时调用:开启 / 关闭 PiP
- (void)togglePiP;
/// 外界在收到远端视频帧回调时调用
- (void)feedVideoFrame:(SellyRTCVideoFrame *)frame;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,213 @@
//
// SellyCallPiPManager.m
// SellyCloudSDK_Example
//
// Created by Caleb on 19/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "SellyCallPiPManager.h"
#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>
@interface SellyCallPiPManager ()
@property (nonatomic, weak) UIView *renderView;
@property (nonatomic, strong) AVSampleBufferDisplayLayer *pipSampleBufferLayer;
@property (nonatomic, strong) AVPictureInPictureController *pipController;
@property (nonatomic, strong) dispatch_queue_t pipQueue;
@property (nonatomic, assign, readwrite) BOOL pipPossible;
@property (nonatomic, assign, readwrite) BOOL pipActive;
@end
@implementation SellyCallPiPManager
- (instancetype)initWithRenderView:(UIView *)renderView {
self = [super init];
if (self) {
_renderView = renderView;
}
return self;
}
- (void)setupIfNeeded {
if (@available(iOS 15.0, *)) {
if (![AVPictureInPictureController isPictureInPictureSupported]) {
self.pipPossible = NO;
return;
}
self.pipQueue = dispatch_queue_create("com.sellycloud.pip.queue", DISPATCH_QUEUE_SERIAL);
// SampleBuffer layer renderView
self.pipSampleBufferLayer = [[AVSampleBufferDisplayLayer alloc] init];
self.pipSampleBufferLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
if (self.renderView) {
self.pipSampleBufferLayer.frame = self.renderView.bounds;
[self.renderView.layer addSublayer:self.pipSampleBufferLayer];
}
// PiP content source
AVPictureInPictureControllerContentSource *source =
[[AVPictureInPictureControllerContentSource alloc] initWithSampleBufferDisplayLayer:self.pipSampleBufferLayer
playbackDelegate:self];
self.pipController = [[AVPictureInPictureController alloc] initWithContentSource:source];
self.pipController.delegate = self;
self.pipPossible = (self.pipController != nil);
} else {
self.pipPossible = NO;
}
}
- (void)togglePiP {
if (@available(iOS 15.0, *)) {
if (!self.pipController || !self.pipController.isPictureInPicturePossible) {
NSLog(@"[PiP] not possible");
return;
}
if (!self.pipController.isPictureInPictureActive) {
[self.pipController startPictureInPicture];
} else {
[self.pipController stopPictureInPicture];
}
}
}
#pragma mark - Feed frame
- (void)feedVideoFrame:(SellyRTCVideoFrame *)frame {
if (!self.pipSampleBufferLayer || !frame.pixelBuffer) return;
if (!self.pipPossible) return;
CMSampleBufferRef sb = [self createSampleBufferFromVideoFrame:frame];
if (!sb) return;
dispatch_async(self.pipQueue, ^{
if (self.pipSampleBufferLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
NSLog(@"[PiP] display layer failed: %@",
self.pipSampleBufferLayer.error);
[self.pipSampleBufferLayer flushAndRemoveImage];
}
[self.pipSampleBufferLayer enqueueSampleBuffer:sb];
CFRelease(sb);
});
}
- (CMSampleBufferRef)createSampleBufferFromVideoFrame:(SellyRTCVideoFrame *)videoData {
if (!videoData || !videoData.pixelBuffer) {
return nil;
}
CVPixelBufferRef pixelBuffer = videoData.pixelBuffer;
CMVideoFormatDescriptionRef videoInfo = NULL;
OSStatus status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
pixelBuffer,
&videoInfo);
if (status != noErr || !videoInfo) {
return nil;
}
CMTime pts;
if (videoData.timestamp > 0) {
// timestamp ns
pts = CMTimeMake(videoData.timestamp, 1000000000);
} else {
CFTimeInterval t = CACurrentMediaTime();
int64_t ms = (int64_t)(t * 1000);
pts = CMTimeMake(ms, 1000);
}
CMSampleTimingInfo timingInfo;
timingInfo.duration = kCMTimeInvalid;
timingInfo.decodeTimeStamp = kCMTimeInvalid;
timingInfo.presentationTimeStamp = pts;
CMSampleBufferRef sampleBuffer = NULL;
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
pixelBuffer,
videoInfo,
&timingInfo,
&sampleBuffer);
CFRelease(videoInfo);
if (status != noErr) {
if (sampleBuffer) {
CFRelease(sampleBuffer);
}
return nil;
}
//
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
if (attachments && CFArrayGetCount(attachments) > 0) {
CFMutableDictionaryRef attachment =
(CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(attachment,
kCMSampleAttachmentKey_DisplayImmediately,
kCFBooleanTrue);
}
return sampleBuffer; // CFRelease
}
#pragma mark - AVPictureInPictureSampleBufferPlaybackDelegate
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController
setPlaying:(BOOL)playing {
NSLog(@"[PiP] setPlaying = %d", playing);
}
- (CMTimeRange)pictureInPictureControllerTimeRangeForPlayback:(AVPictureInPictureController *)pictureInPictureController {
return CMTimeRangeMake(kCMTimeZero, CMTimeMake(INT64_MAX, 1000));
}
- (BOOL)pictureInPictureControllerIsPlaybackPaused:(AVPictureInPictureController *)pictureInPictureController {
return NO;
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController
didTransitionToRenderSize:(CMVideoDimensions)newRenderSize {
NSLog(@"[PiP] render size = %d x %d", newRenderSize.width, newRenderSize.height);
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController
skipByInterval:(CMTime)skipInterval
completionHandler:(void (^)(void))completionHandler {
if (completionHandler) {
completionHandler();
}
}
- (BOOL)pictureInPictureControllerShouldProhibitBackgroundAudioPlayback:(AVPictureInPictureController *)pictureInPictureController {
return NO;
}
- (void)invalidatePlaybackState {
//
}
#pragma mark - AVPictureInPictureControllerDelegate
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"[PiP] will start");
self.pipActive = YES;
}
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"[PiP] did start");
self.pipActive = YES;
}
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
NSLog(@"[PiP] did stop");
self.pipActive = NO;
}
@end

View File

@@ -0,0 +1,19 @@
//
// SellyVideoCallConferenceController.h
// SellyCloudSDK_Example
// 多人会议
// Created by Caleb on 11/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <SellyCloudSDK/SellyCloudManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface SellyVideoCallConferenceController : UIViewController
@property (nonatomic, strong)NSString *channelId;
@property (nonatomic, strong)SellyRTCVideoConfiguration *videoConfig;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,207 @@
//
// 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"
@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;
@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];
//
{
//使receiverspeaker
// [self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker];
}
{
//使receiverspeaker
NSError *error;
[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionDuckOthers|AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionMixWithOthers error:&error];
[self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker];
}
//view
[self attachLocalStream];
self.localAudioEnable = true;
}
- (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];
}
- (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

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="24127" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SellyVideoCallConferenceController">
<connections>
<outlet property="duration" destination="RWd-4c-qBr" id="7Fz-B2-8rP"/>
<outlet property="grid" destination="x73-lB-7SC" id="vbG-wv-e2W"/>
<outlet property="view" destination="iN0-l3-epB" id="bQ9-58-b7b"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="x73-lB-7SC" customClass="SLSVideoGridView">
<rect key="frame" x="0.0" y="118" width="393" height="666"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6v8-oR-9TX">
<rect key="frame" x="10" y="724" width="373" height="60"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9cL-GX-wDb">
<rect key="frame" x="8" y="0.0" width="70.333333333333329" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="扬声器"/>
<connections>
<action selector="onSpeakerClick:" destination="-1" eventType="touchUpInside" id="pdE-NO-Gs7"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jG5-Eu-Uge">
<rect key="frame" x="78.333333333333329" y="0.0" width="69.999999999999986" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="相机"/>
<connections>
<action selector="onCameraClick:" destination="-1" eventType="touchUpInside" id="veg-DA-wqN"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9cJ-6i-CUh">
<rect key="frame" x="154.33333333333334" y="0.0" width="70.333333333333343" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="前/后"/>
<connections>
<action selector="onSwitchClick:" destination="-1" eventType="touchUpInside" id="KDF-UM-l88"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NOc-pM-ipi">
<rect key="frame" x="224.66666666666663" y="0.0" width="70" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="静音"/>
<connections>
<action selector="onMuteClick:" destination="-1" eventType="touchUpInside" id="iM3-2r-3Wa"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="HfC-GS-Nm4">
<rect key="frame" x="294.66666666666669" y="0.0" width="70.333333333333314" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="挂断"/>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="9cJ-6i-CUh" secondAttribute="bottom" id="0qy-He-fOc"/>
<constraint firstItem="HfC-GS-Nm4" firstAttribute="top" secondItem="6v8-oR-9TX" secondAttribute="top" id="10n-bQ-J9U"/>
<constraint firstItem="HfC-GS-Nm4" firstAttribute="width" secondItem="9cL-GX-wDb" secondAttribute="width" id="1eM-d7-N30"/>
<constraint firstAttribute="bottom" secondItem="9cL-GX-wDb" secondAttribute="bottom" id="2IO-yA-oKI"/>
<constraint firstItem="9cL-GX-wDb" firstAttribute="leading" secondItem="6v8-oR-9TX" secondAttribute="leading" constant="8" id="6LO-0b-WAm"/>
<constraint firstItem="9cJ-6i-CUh" firstAttribute="width" secondItem="9cL-GX-wDb" secondAttribute="width" id="6M5-rr-NY3"/>
<constraint firstItem="NOc-pM-ipi" firstAttribute="width" secondItem="9cL-GX-wDb" secondAttribute="width" id="8W2-ba-fCR"/>
<constraint firstAttribute="trailing" secondItem="HfC-GS-Nm4" secondAttribute="trailing" constant="8" id="BJU-bZ-stW"/>
<constraint firstAttribute="height" constant="60" id="GQX-Zk-IU3"/>
<constraint firstAttribute="bottom" secondItem="jG5-Eu-Uge" secondAttribute="bottom" id="JeH-3O-oGh"/>
<constraint firstAttribute="bottom" secondItem="HfC-GS-Nm4" secondAttribute="bottom" id="RDR-5g-dn1"/>
<constraint firstItem="9cJ-6i-CUh" firstAttribute="top" secondItem="6v8-oR-9TX" secondAttribute="top" id="Vcn-tY-tWm"/>
<constraint firstItem="NOc-pM-ipi" firstAttribute="leading" secondItem="9cJ-6i-CUh" secondAttribute="trailing" id="Xlx-TP-7zF"/>
<constraint firstItem="jG5-Eu-Uge" firstAttribute="leading" secondItem="9cL-GX-wDb" secondAttribute="trailing" id="cRF-ab-bZr"/>
<constraint firstItem="jG5-Eu-Uge" firstAttribute="width" secondItem="9cL-GX-wDb" secondAttribute="width" id="dsZ-zI-87w"/>
<constraint firstItem="9cL-GX-wDb" firstAttribute="top" secondItem="6v8-oR-9TX" secondAttribute="top" id="gGo-FE-rhF"/>
<constraint firstItem="HfC-GS-Nm4" firstAttribute="leading" secondItem="NOc-pM-ipi" secondAttribute="trailing" id="nTa-Or-z6z"/>
<constraint firstItem="9cJ-6i-CUh" firstAttribute="leading" secondItem="jG5-Eu-Uge" secondAttribute="trailing" constant="6" id="utX-kz-plt"/>
<constraint firstItem="NOc-pM-ipi" firstAttribute="top" secondItem="6v8-oR-9TX" secondAttribute="top" id="wQY-Jg-zjx"/>
<constraint firstItem="jG5-Eu-Uge" firstAttribute="top" secondItem="6v8-oR-9TX" secondAttribute="top" id="xqc-qc-LBe"/>
<constraint firstAttribute="bottom" secondItem="NOc-pM-ipi" secondAttribute="bottom" id="y0y-MD-VgO"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RWd-4c-qBr">
<rect key="frame" x="196.66666666666666" y="724" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="x73-lB-7SC" secondAttribute="trailing" id="M9E-eI-AK2"/>
<constraint firstItem="6v8-oR-9TX" firstAttribute="top" secondItem="RWd-4c-qBr" secondAttribute="bottom" id="MfF-Tt-Pzc"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="6v8-oR-9TX" secondAttribute="bottom" id="dr1-yC-tY1"/>
<constraint firstItem="x73-lB-7SC" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="emy-ft-ubu"/>
<constraint firstItem="x73-lB-7SC" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="kCD-1e-yr3"/>
<constraint firstItem="RWd-4c-qBr" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="mQ5-Gb-AKH"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="6v8-oR-9TX" secondAttribute="trailing" constant="10" id="ptB-hQ-Xxt"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="x73-lB-7SC" secondAttribute="bottom" id="vYx-gD-7SC"/>
<constraint firstItem="6v8-oR-9TX" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="10" id="xjL-T5-NVc"/>
</constraints>
<point key="canvasLocation" x="140" y="154"/>
</view>
</objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -0,0 +1,19 @@
//
// SellyVideoCallViewController.h
// SellyCloudSDK_Example
// 单聊
// Created by Caleb on 3/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <SellyCloudSDK/SellyCloudManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface SellyVideoCallViewController : UIViewController
@property (nonatomic, strong)NSString *channelId;
@property (nonatomic, strong)SellyRTCVideoConfiguration *videoConfig;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,257 @@
//
// 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];
//
{
//使receiverspeaker
// [self.session setAudioOutput:AVAudioSessionPortOverrideSpeaker];
}
{
//使receiverspeaker
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 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:(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

View File

@@ -0,0 +1,374 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="24127" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SellyVideoCallViewController">
<connections>
<outlet property="bitrate" destination="o8m-iY-37M" id="Zic-rC-qmf"/>
<outlet property="codec" destination="bNV-qQ-j2Q" id="U8l-IE-J0P"/>
<outlet property="duration" destination="cHB-rM-aMb" id="AAH-Lj-wWM"/>
<outlet property="localView" destination="6ee-YL-4qL" id="5uS-Nk-bEw"/>
<outlet property="remoteView" destination="0Nt-EE-wL9" id="NAM-lY-79G"/>
<outlet property="remoteWConstraint" destination="ung-jS-8fk" id="Vbg-pr-YLO"/>
<outlet property="rtt" destination="tlj-eh-tlf" id="tUf-Lz-CEH"/>
<outlet property="videoFps" destination="jSy-UX-E4Z" id="ucx-Ji-mgV"/>
<outlet property="videoSize" destination="Lzv-lv-1qD" id="dGC-J9-aKI"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6ee-YL-4qL">
<rect key="frame" x="0.0" y="118" width="393" height="666"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="j7r-De-oyk">
<rect key="frame" x="12" y="576" width="369" height="60"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="E6T-zb-Wgg">
<rect key="frame" x="8" y="0.0" width="69.333333333333329" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="扬声器"/>
<connections>
<action selector="onSpeakerClick:" destination="-1" eventType="touchUpInside" id="ThQ-g3-tde"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5eT-Kr-XFW">
<rect key="frame" x="77.333333333333343" y="0.0" width="69.333333333333343" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="相机"/>
<connections>
<action selector="onCameraClick:" destination="-1" eventType="touchUpInside" id="kxT-hf-4nV"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BJt-6W-POf">
<rect key="frame" x="152.66666666666666" y="0.0" width="69.666666666666657" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="前/后"/>
<connections>
<action selector="onSwitchClick:" destination="-1" eventType="touchUpInside" id="JiJ-tO-0Bq"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swf-8f-aNA">
<rect key="frame" x="222.33333333333334" y="0.0" width="69.333333333333343" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="静音"/>
<connections>
<action selector="onMuteClick:" destination="-1" eventType="touchUpInside" id="cwb-4v-J15"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LeJ-p8-SAa">
<rect key="frame" x="291.66666666666669" y="0.0" width="69.333333333333314" height="60"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="画中画"/>
<connections>
<action selector="onActionPIP:" destination="-1" eventType="touchUpInside" id="JG3-Zj-bfB"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="LeJ-p8-SAa" secondAttribute="bottom" id="0qg-hu-ZPL"/>
<constraint firstItem="BJt-6W-POf" firstAttribute="width" secondItem="E6T-zb-Wgg" secondAttribute="width" id="2CE-Zj-arY"/>
<constraint firstItem="LeJ-p8-SAa" firstAttribute="width" secondItem="E6T-zb-Wgg" secondAttribute="width" id="2gk-3R-Gxh"/>
<constraint firstAttribute="bottom" secondItem="5eT-Kr-XFW" secondAttribute="bottom" id="5CA-gN-WnZ"/>
<constraint firstAttribute="height" constant="60" id="BUC-Wc-29P"/>
<constraint firstAttribute="bottom" secondItem="E6T-zb-Wgg" secondAttribute="bottom" id="DSl-0W-fPu"/>
<constraint firstItem="5eT-Kr-XFW" firstAttribute="width" secondItem="E6T-zb-Wgg" secondAttribute="width" id="F02-wh-RAD"/>
<constraint firstAttribute="bottom" secondItem="BJt-6W-POf" secondAttribute="bottom" id="Gg4-iA-YVv"/>
<constraint firstItem="swf-8f-aNA" firstAttribute="width" secondItem="E6T-zb-Wgg" secondAttribute="width" id="Iee-Y4-Ma9"/>
<constraint firstItem="swf-8f-aNA" firstAttribute="top" secondItem="j7r-De-oyk" secondAttribute="top" id="NMg-u1-8p2"/>
<constraint firstAttribute="bottom" secondItem="swf-8f-aNA" secondAttribute="bottom" id="OTb-5p-Cqm"/>
<constraint firstAttribute="trailing" secondItem="LeJ-p8-SAa" secondAttribute="trailing" constant="8" id="XB9-cS-Cxo"/>
<constraint firstItem="5eT-Kr-XFW" firstAttribute="top" secondItem="j7r-De-oyk" secondAttribute="top" id="abF-Xi-Pnf"/>
<constraint firstItem="swf-8f-aNA" firstAttribute="leading" secondItem="BJt-6W-POf" secondAttribute="trailing" id="ci1-mn-Au0"/>
<constraint firstItem="E6T-zb-Wgg" firstAttribute="top" secondItem="j7r-De-oyk" secondAttribute="top" id="fyy-eA-9Cb"/>
<constraint firstItem="5eT-Kr-XFW" firstAttribute="leading" secondItem="E6T-zb-Wgg" secondAttribute="trailing" id="gb7-73-du5"/>
<constraint firstItem="LeJ-p8-SAa" firstAttribute="top" secondItem="j7r-De-oyk" secondAttribute="top" id="gi9-4w-v20"/>
<constraint firstItem="BJt-6W-POf" firstAttribute="leading" secondItem="5eT-Kr-XFW" secondAttribute="trailing" constant="6" id="q4b-yq-yTd"/>
<constraint firstItem="E6T-zb-Wgg" firstAttribute="leading" secondItem="j7r-De-oyk" secondAttribute="leading" constant="8" id="riP-t6-v5N"/>
<constraint firstItem="LeJ-p8-SAa" firstAttribute="leading" secondItem="swf-8f-aNA" secondAttribute="trailing" id="v5Y-nz-VcK"/>
<constraint firstItem="BJt-6W-POf" firstAttribute="top" secondItem="j7r-De-oyk" secondAttribute="top" id="zXh-nc-MNi"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemGray6Color"/>
<constraints>
<constraint firstItem="j7r-De-oyk" firstAttribute="leading" secondItem="6ee-YL-4qL" secondAttribute="leading" constant="12" id="ONg-cN-Qi2"/>
<constraint firstAttribute="trailing" secondItem="j7r-De-oyk" secondAttribute="trailing" constant="12" id="Qp1-9f-ofT"/>
<constraint firstAttribute="bottom" secondItem="j7r-De-oyk" secondAttribute="bottom" constant="30" id="j3O-wc-sgB"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="q4E-kH-X2Y">
<rect key="frame" x="0.0" y="128" width="258" height="190"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="W0J-IX-GAt">
<rect key="frame" x="0.0" y="0.0" width="258" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="码率(上/下)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bUd-Fj-vkV">
<rect key="frame" x="10" y="6.6666666666666572" width="75.333333333333329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="o8m-iY-37M">
<rect key="frame" x="217.33333333333334" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="kbps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GUi-6e-Y1D">
<rect key="frame" x="222.33333333333334" y="8" width="27.666666666666657" height="14.333333333333336"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="GUi-6e-Y1D" firstAttribute="centerY" secondItem="W0J-IX-GAt" secondAttribute="centerY" id="CJA-I7-uJ1"/>
<constraint firstAttribute="trailing" secondItem="GUi-6e-Y1D" secondAttribute="trailing" constant="8" id="EOK-k3-ORk"/>
<constraint firstItem="GUi-6e-Y1D" firstAttribute="leading" secondItem="o8m-iY-37M" secondAttribute="trailing" constant="5" id="T6L-T9-9p2"/>
<constraint firstItem="o8m-iY-37M" firstAttribute="centerY" secondItem="W0J-IX-GAt" secondAttribute="centerY" id="dfu-PH-bg6"/>
<constraint firstAttribute="height" constant="30" id="f1y-Hr-CMD"/>
<constraint firstItem="bUd-Fj-vkV" firstAttribute="centerY" secondItem="W0J-IX-GAt" secondAttribute="centerY" id="hPl-s4-sOb"/>
<constraint firstItem="bUd-Fj-vkV" firstAttribute="leading" secondItem="W0J-IX-GAt" secondAttribute="leading" constant="10" id="uPe-N2-wUD"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YyY-da-hEe">
<rect key="frame" x="0.0" y="30" width="258" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="帧率(上/下)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DWM-1c-BOA">
<rect key="frame" x="10" y="6.6666666666666572" width="76" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jSy-UX-E4Z">
<rect key="frame" x="227" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hSL-Ie-ybs">
<rect key="frame" x="232" y="8" width="18" height="14.333333333333336"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="DWM-1c-BOA" firstAttribute="centerY" secondItem="YyY-da-hEe" secondAttribute="centerY" id="V9v-DD-pdg"/>
<constraint firstAttribute="height" constant="30" id="VA2-z3-OmP"/>
<constraint firstItem="DWM-1c-BOA" firstAttribute="leading" secondItem="YyY-da-hEe" secondAttribute="leading" constant="10" id="Xlh-dX-iNH"/>
<constraint firstItem="hSL-Ie-ybs" firstAttribute="centerY" secondItem="YyY-da-hEe" secondAttribute="centerY" id="Yhh-Ui-8y1"/>
<constraint firstItem="hSL-Ie-ybs" firstAttribute="leading" secondItem="jSy-UX-E4Z" secondAttribute="trailing" constant="5" id="hip-UX-ceg"/>
<constraint firstItem="jSy-UX-E4Z" firstAttribute="centerY" secondItem="YyY-da-hEe" secondAttribute="centerY" id="kkz-pQ-YgK"/>
<constraint firstAttribute="trailing" secondItem="hSL-Ie-ybs" secondAttribute="trailing" constant="8" id="pNv-iS-pPG"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="enI-iD-cr0">
<rect key="frame" x="0.0" y="60" width="258" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="rtt" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-IZ-8dQ">
<rect key="frame" x="10" y="6.6666666666666572" width="16" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tlj-eh-tlf">
<rect key="frame" x="228" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ms" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ua6-eQ-PT6">
<rect key="frame" x="233" y="7.6666666666666572" width="17" height="15"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ua6-eQ-PT6" secondAttribute="trailing" constant="8" id="3TR-nO-jw8"/>
<constraint firstItem="tlj-eh-tlf" firstAttribute="centerY" secondItem="enI-iD-cr0" secondAttribute="centerY" id="FVv-O4-Src"/>
<constraint firstItem="i9V-IZ-8dQ" firstAttribute="centerY" secondItem="enI-iD-cr0" secondAttribute="centerY" id="KcM-RI-cEk"/>
<constraint firstItem="ua6-eQ-PT6" firstAttribute="centerY" secondItem="enI-iD-cr0" secondAttribute="centerY" id="N9A-KT-wWQ"/>
<constraint firstItem="ua6-eQ-PT6" firstAttribute="leading" secondItem="tlj-eh-tlf" secondAttribute="trailing" constant="5" id="NcC-tU-Xof"/>
<constraint firstAttribute="height" constant="30" id="dht-1A-7IX"/>
<constraint firstItem="i9V-IZ-8dQ" firstAttribute="leading" secondItem="enI-iD-cr0" secondAttribute="leading" constant="10" id="fFY-l4-BJO"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="e7M-aI-Og6">
<rect key="frame" x="0.0" y="90" width="258" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="codec" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UB0-g3-9v2">
<rect key="frame" x="10" y="6.6666666666666572" width="40" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bNV-qQ-j2Q">
<rect key="frame" x="245" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="s5F-dm-vF7">
<rect key="frame" x="250" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="s5F-dm-vF7" firstAttribute="centerY" secondItem="e7M-aI-Og6" secondAttribute="centerY" id="6SI-So-g0w"/>
<constraint firstItem="bNV-qQ-j2Q" firstAttribute="centerY" secondItem="e7M-aI-Og6" secondAttribute="centerY" id="6qb-tI-YBQ"/>
<constraint firstAttribute="height" constant="30" id="A1b-72-Gpa"/>
<constraint firstItem="s5F-dm-vF7" firstAttribute="leading" secondItem="bNV-qQ-j2Q" secondAttribute="trailing" constant="5" id="eo8-9P-sd8"/>
<constraint firstAttribute="trailing" secondItem="s5F-dm-vF7" secondAttribute="trailing" constant="8" id="ilK-u8-Z0r"/>
<constraint firstItem="UB0-g3-9v2" firstAttribute="leading" secondItem="e7M-aI-Og6" secondAttribute="leading" constant="10" id="nxe-KP-x0t"/>
<constraint firstItem="UB0-g3-9v2" firstAttribute="centerY" secondItem="e7M-aI-Og6" secondAttribute="centerY" id="wMu-kL-cGg"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dnX-kX-w8j">
<rect key="frame" x="0.0" y="120" width="258" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="通话时长" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MHg-b6-byJ">
<rect key="frame" x="10" y="6.6666666666666572" width="56" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cHB-rM-aMb">
<rect key="frame" x="245" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dcf-dl-LNl">
<rect key="frame" x="250" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="06W-pj-uQh"/>
<constraint firstItem="dcf-dl-LNl" firstAttribute="leading" secondItem="cHB-rM-aMb" secondAttribute="trailing" constant="5" id="4IX-9t-pVV"/>
<constraint firstItem="cHB-rM-aMb" firstAttribute="centerY" secondItem="dnX-kX-w8j" secondAttribute="centerY" id="CQu-bi-7Uj"/>
<constraint firstItem="MHg-b6-byJ" firstAttribute="leading" secondItem="dnX-kX-w8j" secondAttribute="leading" constant="10" id="RwP-bg-mcu"/>
<constraint firstItem="dcf-dl-LNl" firstAttribute="centerY" secondItem="dnX-kX-w8j" secondAttribute="centerY" id="SNA-ps-e3o"/>
<constraint firstItem="MHg-b6-byJ" firstAttribute="centerY" secondItem="dnX-kX-w8j" secondAttribute="centerY" id="Y7v-oO-vc0"/>
<constraint firstAttribute="trailing" secondItem="dcf-dl-LNl" secondAttribute="trailing" constant="8" id="jJR-hu-qe2"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="HFE-M1-5pt">
<rect key="frame" x="0.0" y="150" width="258" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="视频:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gpc-qR-pYU">
<rect key="frame" x="10" y="6.6666666666666856" width="32" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Lzv-lv-1qD">
<rect key="frame" x="245" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="A1X-q5-YCb">
<rect key="frame" x="250" y="15" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="A1X-q5-YCb" secondAttribute="trailing" constant="8" id="52k-c0-yrI"/>
<constraint firstItem="A1X-q5-YCb" firstAttribute="leading" secondItem="Lzv-lv-1qD" secondAttribute="trailing" constant="5" id="Jf6-1g-FHi"/>
<constraint firstItem="A1X-q5-YCb" firstAttribute="centerY" secondItem="HFE-M1-5pt" secondAttribute="centerY" id="OdN-yN-mDR"/>
<constraint firstItem="gpc-qR-pYU" firstAttribute="leading" secondItem="HFE-M1-5pt" secondAttribute="leading" constant="10" id="PCa-gk-rvl"/>
<constraint firstAttribute="height" constant="30" id="UH8-fN-c7w"/>
<constraint firstItem="Lzv-lv-1qD" firstAttribute="centerY" secondItem="HFE-M1-5pt" secondAttribute="centerY" id="dJh-LJ-wSy"/>
<constraint firstItem="gpc-qR-pYU" firstAttribute="centerY" secondItem="HFE-M1-5pt" secondAttribute="centerY" id="oPH-H3-oI9"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.20000000000000001" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="YyY-da-hEe" secondAttribute="trailing" id="3Gr-nx-pd0"/>
<constraint firstItem="HFE-M1-5pt" firstAttribute="top" secondItem="dnX-kX-w8j" secondAttribute="bottom" id="4K0-r2-5x7"/>
<constraint firstAttribute="trailing" secondItem="dnX-kX-w8j" secondAttribute="trailing" id="4go-fm-OFx"/>
<constraint firstItem="W0J-IX-GAt" firstAttribute="leading" secondItem="q4E-kH-X2Y" secondAttribute="leading" id="6Jy-oI-BNH"/>
<constraint firstItem="YyY-da-hEe" firstAttribute="leading" secondItem="q4E-kH-X2Y" secondAttribute="leading" id="7ka-Nr-Pan"/>
<constraint firstAttribute="trailing" secondItem="e7M-aI-Og6" secondAttribute="trailing" id="Agh-Zn-p2K"/>
<constraint firstItem="dnX-kX-w8j" firstAttribute="leading" secondItem="q4E-kH-X2Y" secondAttribute="leading" id="KCQ-pA-oC4"/>
<constraint firstAttribute="trailing" secondItem="HFE-M1-5pt" secondAttribute="trailing" id="KvH-vj-fOb"/>
<constraint firstItem="e7M-aI-Og6" firstAttribute="leading" secondItem="q4E-kH-X2Y" secondAttribute="leading" id="NPl-GU-z7C"/>
<constraint firstAttribute="height" constant="190" id="NaF-I1-CL9"/>
<constraint firstItem="YyY-da-hEe" firstAttribute="top" secondItem="W0J-IX-GAt" secondAttribute="bottom" id="PCA-Bm-d3u"/>
<constraint firstItem="dnX-kX-w8j" firstAttribute="top" secondItem="e7M-aI-Og6" secondAttribute="bottom" id="Pnr-sZ-ijZ"/>
<constraint firstItem="HFE-M1-5pt" firstAttribute="leading" secondItem="q4E-kH-X2Y" secondAttribute="leading" id="S8M-r7-z0a"/>
<constraint firstItem="enI-iD-cr0" firstAttribute="top" secondItem="YyY-da-hEe" secondAttribute="bottom" id="VZr-fz-115"/>
<constraint firstItem="e7M-aI-Og6" firstAttribute="top" secondItem="enI-iD-cr0" secondAttribute="bottom" id="YAp-Go-xtO"/>
<constraint firstItem="enI-iD-cr0" firstAttribute="leading" secondItem="q4E-kH-X2Y" secondAttribute="leading" id="kmB-XH-QLA"/>
<constraint firstAttribute="trailing" secondItem="enI-iD-cr0" secondAttribute="trailing" id="qJA-sF-3EP"/>
<constraint firstItem="W0J-IX-GAt" firstAttribute="top" secondItem="q4E-kH-X2Y" secondAttribute="top" id="rAm-5c-zHG"/>
<constraint firstAttribute="trailing" secondItem="W0J-IX-GAt" secondAttribute="trailing" id="v5L-Be-S1P"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0Nt-EE-wL9">
<rect key="frame" x="263" y="128" width="120" height="200"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="QBt-ql-Qt9"/>
<constraint firstAttribute="width" constant="120" id="ung-jS-8fk"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="Q5M-cg-NOt"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="q4E-kH-X2Y" firstAttribute="leading" secondItem="Q5M-cg-NOt" secondAttribute="leading" id="1dy-Gf-N5D"/>
<constraint firstItem="0Nt-EE-wL9" firstAttribute="top" secondItem="6ee-YL-4qL" secondAttribute="top" constant="10" id="6Ul-Xf-I75"/>
<constraint firstItem="0Nt-EE-wL9" firstAttribute="trailing" secondItem="6ee-YL-4qL" secondAttribute="trailing" constant="-10" id="McM-1X-FjP"/>
<constraint firstItem="6ee-YL-4qL" firstAttribute="leading" secondItem="Q5M-cg-NOt" secondAttribute="leading" id="Vfb-hu-Vjl"/>
<constraint firstItem="6ee-YL-4qL" firstAttribute="top" secondItem="Q5M-cg-NOt" secondAttribute="top" id="ZqC-VR-rnk"/>
<constraint firstItem="Q5M-cg-NOt" firstAttribute="bottom" secondItem="6ee-YL-4qL" secondAttribute="bottom" id="dao-WN-deP"/>
<constraint firstItem="0Nt-EE-wL9" firstAttribute="leading" secondItem="q4E-kH-X2Y" secondAttribute="trailing" constant="5" id="jwe-oS-X1g"/>
<constraint firstItem="q4E-kH-X2Y" firstAttribute="top" secondItem="0Nt-EE-wL9" secondAttribute="top" id="xeA-XP-OU3"/>
<constraint firstItem="Q5M-cg-NOt" firstAttribute="trailing" secondItem="6ee-YL-4qL" secondAttribute="trailing" id="yuA-Bt-5p9"/>
</constraints>
<point key="canvasLocation" x="-2422.1374045801526" y="-228.16901408450704"/>
</view>
</objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemGray6Color">
<color red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -0,0 +1,20 @@
//
// TokenGenerator.h
// SellyCloudSDK_Example
//
// Created by Caleb on 20/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonCrypto.h>
NS_ASSUME_NONNULL_BEGIN
@interface TokenGenerator : NSObject
+ (NSString *)generateTokenWithUserId:(NSString *)userId
callId:(NSString *)callId;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,48 @@
//
// TokenGenerator.m
// SellyCloudSDK_Example
//
// Created by Caleb on 20/11/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "TokenGenerator.h"
#define KAppId @"demo-app"
#define KSecret @"CHANGE_ME"
@implementation TokenGenerator
+ (NSString *)generateTokenWithUserId:(NSString *)userId
callId:(NSString *)callId
{
//
long signTime = (long)[[NSDate date] timeIntervalSince1970];
long exprTime = signTime + 600; // 10
// payload
NSString *payload = [NSString stringWithFormat:@"%@%@%@%ld%ld",
KAppId, userId, callId, signTime, exprTime];
// HMAC-SHA256
const char *cKey = [KSecret cStringUsingEncoding:NSUTF8StringEncoding];
const char *cData = [payload cStringUsingEncoding:NSUTF8StringEncoding];
unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), cHMAC);
// hex
NSMutableString *sign = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[sign appendFormat:@"%02x", cHMAC[i]];
}
// Token
NSString *token = [NSString stringWithFormat:
@"appid=%@&userid=%@&callid=%@&signtime=%ld&exprtime=%ld&sign=%@",
KAppId, userId, callId, signTime, exprTime, sign];
return token;
}
@end