init
This commit is contained in:
29
Example/SellyCloudSDK/VideoCall/SLSParticipant.h
Normal file
29
Example/SellyCloudSDK/VideoCall/SLSParticipant.h
Normal 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
|
||||
13
Example/SellyCloudSDK/VideoCall/SLSParticipant.m
Normal file
13
Example/SellyCloudSDK/VideoCall/SLSParticipant.m
Normal 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
|
||||
38
Example/SellyCloudSDK/VideoCall/SLSVideoGridView.h
Normal file
38
Example/SellyCloudSDK/VideoCall/SLSVideoGridView.h
Normal 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
|
||||
180
Example/SellyCloudSDK/VideoCall/SLSVideoGridView.m
Normal file
180
Example/SellyCloudSDK/VideoCall/SLSVideoGridView.m
Normal 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/2、1/3、1/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
|
||||
27
Example/SellyCloudSDK/VideoCall/SLSVideoTileView.h
Normal file
27
Example/SellyCloudSDK/VideoCall/SLSVideoTileView.h
Normal 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
|
||||
194
Example/SellyCloudSDK/VideoCall/SLSVideoTileView.m
Normal file
194
Example/SellyCloudSDK/VideoCall/SLSVideoTileView.m
Normal 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
|
||||
42
Example/SellyCloudSDK/VideoCall/SellyCallPiPManager.h
Normal file
42
Example/SellyCloudSDK/VideoCall/SellyCallPiPManager.h
Normal 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
|
||||
213
Example/SellyCloudSDK/VideoCall/SellyCallPiPManager.m
Normal file
213
Example/SellyCloudSDK/VideoCall/SellyCallPiPManager.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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];
|
||||
|
||||
//设置扬声器播放
|
||||
{
|
||||
//使用这种方案,通话接通前无法在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;
|
||||
}
|
||||
|
||||
- (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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
257
Example/SellyCloudSDK/VideoCall/SellyVideoCallViewController.m
Normal file
257
Example/SellyCloudSDK/VideoCall/SellyVideoCallViewController.m
Normal 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];
|
||||
|
||||
//设置扬声器播放
|
||||
{
|
||||
//使用这种方案,通话接通前无法在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
|
||||
374
Example/SellyCloudSDK/VideoCall/SellyVideoCallViewController.xib
Normal file
374
Example/SellyCloudSDK/VideoCall/SellyVideoCallViewController.xib
Normal 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>
|
||||
20
Example/SellyCloudSDK/VideoCall/TokenGenerator.h
Normal file
20
Example/SellyCloudSDK/VideoCall/TokenGenerator.h
Normal 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
|
||||
48
Example/SellyCloudSDK/VideoCall/TokenGenerator.m
Normal file
48
Example/SellyCloudSDK/VideoCall/TokenGenerator.m
Normal 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
|
||||
Reference in New Issue
Block a user