initial commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// AVCallViewController.h
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVCallViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
223
Example/SellyCloudSDK/Controllers/Home/AVCallViewController.m
Normal file
223
Example/SellyCloudSDK/Controllers/Home/AVCallViewController.m
Normal file
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// AVCallViewController.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVCallViewController.h"
|
||||
#import "SellyVideoCallViewController.h"
|
||||
#import "SellyVideoCallConferenceController.h"
|
||||
#import "AVConfigManager.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface AVCallViewController () <UITextFieldDelegate>
|
||||
@property (nonatomic, strong) UITextField *channelIdTextField;
|
||||
@property (nonatomic, strong) UIButton *callButton;
|
||||
@property (nonatomic, strong) UIButton *conferenceButton;
|
||||
@property (nonatomic, strong) UIView *containerView;
|
||||
@end
|
||||
|
||||
@implementation AVCallViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor systemBackgroundColor];
|
||||
self.title = @"通话";
|
||||
|
||||
[self setupUI];
|
||||
|
||||
// 加载上次使用的频道ID(优先使用单聊的,如果没有则使用会议的)
|
||||
NSString *lastChannelId = [[AVConfigManager sharedManager] loadCallChannelId];
|
||||
if (lastChannelId.length == 0) {
|
||||
lastChannelId = [[AVConfigManager sharedManager] loadConferenceChannelId];
|
||||
}
|
||||
self.channelIdTextField.text = lastChannelId;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
// 创建容器视图
|
||||
self.containerView = [[UIView alloc] init];
|
||||
[self.view addSubview:self.containerView];
|
||||
|
||||
// 创建频道ID标签
|
||||
UILabel *titleLabel = [[UILabel alloc] init];
|
||||
titleLabel.text = @"频道ID";
|
||||
titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
titleLabel.textColor = [UIColor labelColor];
|
||||
[self.containerView addSubview:titleLabel];
|
||||
|
||||
// 创建频道ID输入框
|
||||
self.channelIdTextField = [[UITextField alloc] init];
|
||||
self.channelIdTextField.placeholder = @"请输入频道ID";
|
||||
self.channelIdTextField.borderStyle = UITextBorderStyleRoundedRect;
|
||||
self.channelIdTextField.font = [UIFont systemFontOfSize:16];
|
||||
self.channelIdTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
||||
self.channelIdTextField.returnKeyType = UIReturnKeyDone;
|
||||
self.channelIdTextField.delegate = self;
|
||||
self.channelIdTextField.backgroundColor = [UIColor secondarySystemBackgroundColor];
|
||||
[self.containerView addSubview:self.channelIdTextField];
|
||||
|
||||
// 创建音视频单聊按钮
|
||||
self.callButton = [self createButtonWithTitle:@"音视频单聊"
|
||||
icon:@"video"
|
||||
action:@selector(callTapped)];
|
||||
[self.containerView addSubview:self.callButton];
|
||||
|
||||
// 创建音视频会议按钮
|
||||
self.conferenceButton = [self createButtonWithTitle:@"音视频会议"
|
||||
icon:@"person.3"
|
||||
action:@selector(conferenceTapped)];
|
||||
[self.containerView addSubview:self.conferenceButton];
|
||||
|
||||
// 布局
|
||||
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self.view);
|
||||
make.width.equalTo(@320);
|
||||
}];
|
||||
|
||||
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.left.equalTo(self.containerView);
|
||||
make.right.equalTo(self.containerView);
|
||||
}];
|
||||
|
||||
[self.channelIdTextField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(titleLabel.mas_bottom).offset(8);
|
||||
make.left.right.equalTo(self.containerView);
|
||||
make.height.equalTo(@44);
|
||||
}];
|
||||
|
||||
[self.callButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.channelIdTextField.mas_bottom).offset(32);
|
||||
make.left.right.equalTo(self.containerView);
|
||||
make.height.equalTo(@100);
|
||||
}];
|
||||
|
||||
[self.conferenceButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.callButton.mas_bottom).offset(16);
|
||||
make.left.right.equalTo(self.containerView);
|
||||
make.height.equalTo(@100);
|
||||
make.bottom.equalTo(self.containerView);
|
||||
}];
|
||||
}
|
||||
|
||||
- (UIButton *)createButtonWithTitle:(NSString *)title icon:(NSString *)iconName action:(SEL)action {
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
button.backgroundColor = [UIColor systemBlueColor];
|
||||
button.layer.cornerRadius = 12;
|
||||
button.clipsToBounds = YES;
|
||||
|
||||
// 创建图标
|
||||
UIImageView *iconView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:iconName]];
|
||||
iconView.tintColor = [UIColor whiteColor];
|
||||
iconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
|
||||
// 创建标题
|
||||
UILabel *titleLabel = [[UILabel alloc] init];
|
||||
titleLabel.text = title;
|
||||
titleLabel.textColor = [UIColor whiteColor];
|
||||
titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
||||
titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
|
||||
[button addSubview:iconView];
|
||||
[button addSubview:titleLabel];
|
||||
|
||||
[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(button);
|
||||
make.centerY.equalTo(button).offset(-15);
|
||||
make.width.height.equalTo(@40);
|
||||
}];
|
||||
|
||||
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(button);
|
||||
make.top.equalTo(iconView.mas_bottom).offset(8);
|
||||
make.left.greaterThanOrEqualTo(button).offset(8);
|
||||
make.right.lessThanOrEqualTo(button).offset(-8);
|
||||
}];
|
||||
|
||||
[button addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
#pragma mark - Button Actions
|
||||
|
||||
- (void)callTapped {
|
||||
NSString *channelId = [self.channelIdTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
// 验证输入
|
||||
if (channelId.length == 0) {
|
||||
[self showErrorAlert:@"频道ID不能为空"];
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存频道 ID
|
||||
[[AVConfigManager sharedManager] saveCallChannelId:channelId];
|
||||
|
||||
// 创建并跳转到视频通话页面
|
||||
SellyVideoCallViewController *vc = [[SellyVideoCallViewController alloc] init];
|
||||
vc.channelId = channelId;
|
||||
vc.videoConfig = SellyRTCVideoConfiguration.defaultConfig;
|
||||
|
||||
// 隐藏底部 TabBar
|
||||
vc.hidesBottomBarWhenPushed = YES;
|
||||
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
- (void)conferenceTapped {
|
||||
NSString *channelId = [self.channelIdTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
// 验证输入
|
||||
if (channelId.length == 0) {
|
||||
[self showErrorAlert:@"频道ID不能为空"];
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存频道 ID
|
||||
[[AVConfigManager sharedManager] saveConferenceChannelId:channelId];
|
||||
|
||||
// 创建并跳转到视频会议页面
|
||||
SellyVideoCallConferenceController *vc = [[SellyVideoCallConferenceController alloc] init];
|
||||
vc.channelId = channelId;
|
||||
vc.videoConfig = SellyRTCVideoConfiguration.defaultConfig;
|
||||
|
||||
// 隐藏底部 TabBar
|
||||
vc.hidesBottomBarWhenPushed = YES;
|
||||
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
#pragma mark - Helper Methods
|
||||
|
||||
- (void)showErrorAlert:(NSString *)message {
|
||||
UIAlertController *errorAlert = [UIAlertController alertControllerWithTitle:@"错误"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:nil];
|
||||
[errorAlert addAction:okAction];
|
||||
[self presentViewController:errorAlert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - UITextFieldDelegate
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
[textField resignFirstResponder];
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Orientation Support (强制只支持竖屏)
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
return UIInterfaceOrientationPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// AVHomeViewController.h
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVHomeViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
393
Example/SellyCloudSDK/Controllers/Home/AVHomeViewController.m
Normal file
393
Example/SellyCloudSDK/Controllers/Home/AVHomeViewController.m
Normal file
@@ -0,0 +1,393 @@
|
||||
//
|
||||
// AVHomeViewController.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVHomeViewController.h"
|
||||
#import "SCLivePusherViewController.h"
|
||||
#import "SCLiveVideoPlayerViewController.h"
|
||||
#import "SCVodVideoPlayerViewController.h"
|
||||
#import "AVConfigManager.h"
|
||||
#import "AVConstants.h"
|
||||
#import "AVLiveStreamModel.h"
|
||||
#import "AVLiveStreamCell.h"
|
||||
#import "SCPlayerConfigView.h"
|
||||
#import <AFNetworking/AFNetworking.h>
|
||||
#import <AFNetworking/UIImageView+AFNetworking.h>
|
||||
#import <YYModel/YYModel.h>
|
||||
|
||||
@interface AVHomeViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
|
||||
@property (nonatomic, strong) UIView *headerView;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *headerButtons;
|
||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||
@property (nonatomic, strong) NSArray<AVLiveStreamModel *> *liveStreams;
|
||||
@property (nonatomic, strong) UIRefreshControl *refreshControl;
|
||||
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;
|
||||
@end
|
||||
|
||||
@implementation AVHomeViewController
|
||||
|
||||
static NSString * const kLiveStreamCellIdentifier = @"LiveStreamCell";
|
||||
static NSString * const kLiveListAPIURL = @"http://rtmp.sellycloud.io:8089/live/sdk/alive-list";
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor systemBackgroundColor];
|
||||
self.title = @"首页";
|
||||
|
||||
const char *kiwiAppkey = "5XTXUZ/aqOwfjA4zQkY7VpjcNBucWxmNGY4vFNhwSMKWkn2WK383dbNgI+96Y+ttSPMFzqhu8fxP5SiCK5+/6cGrBQQt8pDQAOi3EN4Z6lzkC2cJ5mfjBVi4ZpFASG9e3divF5UqLG6sTmFI3eCuJxy9/kHXPSSkKWJe1MnBMQETpf4FRDVuR9d/LzXKQgA9PsjRbPRLx4f3h0TU2P4GEfv1c70FvkdwpqirQt9ik2hAhKuj0vJY60g+yYhGY19a07vBTW4MprN53RnSH8bCs79NNbWyzsg2++t+sKdZP1WPGeOho/xpsQRP8yWCXIOOdvdjiE3YXVltBgmPnA6gOjFS97WVlBAQ1mJE7rQi+/5hhfTuJlWoBH6000SRe7dc5EA0WGQX9U1Aj96ahBQhyHTrHJySmJ/hRMYMudqByF6K4PtrwZ8zugTjtx1dyLPOonZDlTu7hPAIcUfuaQ9xS3Phbq8lP67EYDsr3pkWuwL6AjrPjFwNmi0P1g+hV1ZQUmDQVGhNHmF3cE9Pd5ZOS10/fwaXYGRhcq9PlUSmcbU3scLtrBlzpOslyjlQ6W57EudCrvvJU3mimfs1A2y7cjpnLlJN1CWh6dQAaGcwSG2QA8+88qmlMH1t627fItTgHYrP1DkExpAr2dqgYDvsICJnHaRSBMe608GrPbFaECutRz5y3BEtQKcVKdgA1e6W4TFnxs5HqGrzc8iHPOOKGf8zHWEXkITPBKEiA86Nz46pDrqM9FKx4upPijn4Dahj8pd7yWTUIdHBT8X39Vm3/TSV5xT/lTinmv8rhBieb/2SQamTjVQ22VFq3nQ1h4TxUYTEc0nSjqcz54fWf1cyBy7uh82q1weKXUAJ8vG9W05vmt3/aDZ9+C8cWm53AQ90xgDvW7M1mZveuyfof2qrPsXTpj+jhpDkJgm6qJsvV5ClmGth8gvCM0rHjSIwxhYDZaIDK5TkFWjwLltt+YhhYLKketwuTHdlO/hCxrsFzlXHhXGVRC+kgXusfQUrHIm1WjW9o9EqasHg9ufUgg7cMO/9FRZhJ+Xdw9erprYDvu84Da9jL6NUUOSNIGTCJ/s29Lz4SIwCVG2lzm2UhD6E9ipGfG9gc6e/2vt1emOsP3/ipHVJf16r/9S4+dGKIjPX6QcHIIL2AMu2Je07nPmEoz7KaeOShox4bG3puMQdkdQo6kRIFpUzwUty+4EWqHmyPHGkGGGfI8gj0EreiZwgVJmBQ/8S5wlK+iUp+TVeoXo=";
|
||||
[SellyCloudManager.sharedInstance startWithVHost:V_HOST appName:APP_ID];
|
||||
//初始化洋葱盾相关
|
||||
[SellyCloudManager setKiwiAppKey:kiwiAppkey name:"123"];
|
||||
|
||||
// 生成随机 userId: user + 3位随机数字 (001-999)
|
||||
NSInteger randomNum = arc4random_uniform(999) + 1; // 生成 1-999 的随机数
|
||||
SellyCloudManager.sharedInstance.userId = [NSString stringWithFormat:@"user%03ld", (long)randomNum]; // %03ld 保证3位数,不足补0
|
||||
|
||||
// 初始化数据
|
||||
self.liveStreams = [NSMutableArray array];
|
||||
|
||||
// 设置 AFNetworking
|
||||
self.sessionManager = [AFHTTPSessionManager manager];
|
||||
self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
|
||||
self.sessionManager.requestSerializer.timeoutInterval = 10;
|
||||
|
||||
[self setupHeaderButtons];
|
||||
[self setupCollectionView];
|
||||
|
||||
// 首次加载数据
|
||||
[self fetchLiveStreams];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
[super viewWillLayoutSubviews];
|
||||
// CollectionView 会自动根据 layout 调整
|
||||
}
|
||||
|
||||
#pragma mark - Setup UI
|
||||
|
||||
- (void)setupHeaderButtons {
|
||||
// Create 2 feature buttons
|
||||
NSArray *buttonConfigs = @[
|
||||
@{@"title": @"开始直播", @"icon": @"antenna.radiowaves.left.and.right", @"action": @"livePushTapped"},
|
||||
@{@"title": @"自定义播放", @"icon": @"play.rectangle", @"action": @"livePullTapped"}
|
||||
];
|
||||
|
||||
// 创建 header 容器 View
|
||||
self.headerView = [[UIView alloc] init];
|
||||
self.headerView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
[self.view addSubview:self.headerView];
|
||||
|
||||
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
|
||||
make.left.right.equalTo(self.view);
|
||||
make.height.equalTo(@80);
|
||||
}];
|
||||
|
||||
// 创建按钮
|
||||
NSMutableArray<UIButton *> *buttons = [NSMutableArray array];
|
||||
for (NSInteger i = 0; i < buttonConfigs.count; i++) {
|
||||
NSDictionary *config = buttonConfigs[i];
|
||||
UIButton *button = [self createHeaderButtonWithConfig:config];
|
||||
[self.headerView addSubview:button];
|
||||
[buttons addObject:button];
|
||||
|
||||
[button mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerY.equalTo(self.headerView);
|
||||
make.height.equalTo(@60);
|
||||
|
||||
if (i == 0) {
|
||||
// 第一个按钮:左边距16,右边到中点偏左6(总间距12的一半)
|
||||
make.left.equalTo(self.headerView).offset(16);
|
||||
make.right.equalTo(self.headerView.mas_centerX).offset(-6);
|
||||
} else if (i == 1) {
|
||||
// 第二个按钮:左边从中点偏右6,右边距16
|
||||
make.left.equalTo(self.headerView.mas_centerX).offset(6);
|
||||
make.right.equalTo(self.headerView).offset(-16);
|
||||
}
|
||||
}];
|
||||
}
|
||||
self.headerButtons = [buttons copy];
|
||||
}
|
||||
|
||||
- (void)setupCollectionView {
|
||||
// 创建 UICollectionViewFlowLayout
|
||||
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
||||
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
|
||||
layout.minimumInteritemSpacing = 12;
|
||||
layout.minimumLineSpacing = 16;
|
||||
layout.sectionInset = UIEdgeInsetsMake(16, 16, 16, 16);
|
||||
|
||||
// 创建 CollectionView
|
||||
self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
||||
self.collectionView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
self.collectionView.delegate = self;
|
||||
self.collectionView.dataSource = self;
|
||||
self.collectionView.alwaysBounceVertical = YES;
|
||||
|
||||
// 注册自定义 Cell
|
||||
[self.collectionView registerClass:[AVLiveStreamCell class] forCellWithReuseIdentifier:kLiveStreamCellIdentifier];
|
||||
|
||||
[self.view addSubview:self.collectionView];
|
||||
|
||||
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.headerView.mas_bottom);
|
||||
make.left.right.bottom.equalTo(self.view);
|
||||
}];
|
||||
|
||||
// 添加下拉刷新
|
||||
self.refreshControl = [[UIRefreshControl alloc] init];
|
||||
[self.refreshControl addTarget:self action:@selector(handleRefresh:) forControlEvents:UIControlEventValueChanged];
|
||||
self.collectionView.refreshControl = self.refreshControl;
|
||||
}
|
||||
|
||||
- (UIButton *)createHeaderButtonWithConfig:(NSDictionary *)config {
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
button.backgroundColor = [UIColor systemBlueColor];
|
||||
button.layer.cornerRadius = 10;
|
||||
button.clipsToBounds = YES;
|
||||
|
||||
// Create icon and title
|
||||
UIImageView *iconView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:config[@"icon"]]];
|
||||
iconView.tintColor = [UIColor whiteColor];
|
||||
iconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
|
||||
UILabel *titleLabel = [[UILabel alloc] init];
|
||||
titleLabel.text = config[@"title"];
|
||||
titleLabel.textColor = [UIColor whiteColor];
|
||||
titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
|
||||
titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
|
||||
[button addSubview:iconView];
|
||||
[button addSubview:titleLabel];
|
||||
|
||||
[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(button);
|
||||
make.centerY.equalTo(button).offset(-10);
|
||||
make.width.height.equalTo(@28);
|
||||
}];
|
||||
|
||||
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(button);
|
||||
make.top.equalTo(iconView.mas_bottom).offset(4);
|
||||
make.left.greaterThanOrEqualTo(button).offset(8);
|
||||
make.right.lessThanOrEqualTo(button).offset(-8);
|
||||
}];
|
||||
|
||||
SEL action = NSSelectorFromString(config[@"action"]);
|
||||
[button addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
#pragma mark - Network Request
|
||||
|
||||
- (void)fetchLiveStreams {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
|
||||
[self.sessionManager GET:kLiveListAPIURL parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary *responseObject) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
NSLog(@"%@",responseObject);
|
||||
[strongSelf.refreshControl endRefreshing];
|
||||
// 解析数据
|
||||
strongSelf.liveStreams = [NSArray yy_modelArrayWithClass:AVLiveStreamModel.class json:responseObject[@"list"]];
|
||||
|
||||
NSLog(@"✅ 成功获取 %ld 个直播流", (long)strongSelf.liveStreams.count);
|
||||
|
||||
// 刷新 UI
|
||||
[strongSelf.collectionView reloadData];
|
||||
|
||||
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
|
||||
[strongSelf.refreshControl endRefreshing];
|
||||
|
||||
NSLog(@"❌ 网络请求失败: %@", error.localizedDescription);
|
||||
|
||||
// 显示错误提示
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
|
||||
message:[NSString stringWithFormat:@"无法获取直播列表: %@", error.localizedDescription]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[strongSelf presentViewController:alert animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleRefresh:(UIRefreshControl *)refreshControl {
|
||||
[self fetchLiveStreams];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDataSource
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.liveStreams.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
AVLiveStreamCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kLiveStreamCellIdentifier forIndexPath:indexPath];
|
||||
|
||||
AVLiveStreamModel *model = self.liveStreams[indexPath.item];
|
||||
|
||||
// 配置 Cell
|
||||
[cell configureWithModel:model];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDelegateFlowLayout
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
// 每行显示 2 个,间距 12pt,左右边距各 16pt
|
||||
CGFloat totalHorizontalPadding = 16 * 2 + 12; // left + right + middle spacing
|
||||
CGFloat availableWidth = collectionView.bounds.size.width - totalHorizontalPadding;
|
||||
CGFloat itemWidth = availableWidth / 2.0;
|
||||
|
||||
// 缩略图宽高比 3:4
|
||||
CGFloat thumbnailHeight = itemWidth * (4.0 / 3.0);
|
||||
|
||||
// 计算总高度:
|
||||
// thumbnailHeight + 8 (spacing) + 17 (nameLabel) + 4 (spacing) + 16 (infoContainer) + 8 (bottom padding)
|
||||
CGFloat itemHeight = thumbnailHeight + 8 + 17 + 4 + 16 + 8;
|
||||
|
||||
return CGSizeMake(itemWidth, itemHeight);
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDelegate
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
|
||||
|
||||
AVLiveStreamModel *model = self.liveStreams[indexPath.item];
|
||||
|
||||
NSLog(@"🎬 点击播放 - stream: %@, protocol: %@", model.displayName, model.play_protocol);
|
||||
|
||||
// 直接使用模型跳转到播放器
|
||||
SCLiveVideoPlayerViewController *vc = [[SCLiveVideoPlayerViewController alloc] initWithLiveStream:model];
|
||||
vc.hidesBottomBarWhenPushed = YES;
|
||||
vc.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
#pragma mark - Button Actions
|
||||
|
||||
- (void)livePushTapped {
|
||||
// 创建 action sheet 让用户选择推流协议
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"开始直播"
|
||||
message:@"请选择推流协议"
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
// RTC 协议
|
||||
UIAlertAction *rtcAction = [UIAlertAction actionWithTitle:@"RTC 协议"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
[self startLivePushWithProtocol:AVStreamProtocolRTC];
|
||||
}];
|
||||
[alert addAction:rtcAction];
|
||||
|
||||
// RTMP 协议
|
||||
UIAlertAction *rtmpAction = [UIAlertAction actionWithTitle:@"RTMP 协议"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
[self startLivePushWithProtocol:AVStreamProtocolRTMP];
|
||||
}];
|
||||
[alert addAction:rtmpAction];
|
||||
|
||||
// 取消按钮
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
[alert addAction:cancelAction];
|
||||
|
||||
// 在 iPad 上需要设置 popoverPresentationController
|
||||
if (alert.popoverPresentationController) {
|
||||
alert.popoverPresentationController.sourceView = self.view;
|
||||
alert.popoverPresentationController.sourceRect = CGRectMake(self.view.bounds.size.width / 2.0,
|
||||
self.view.bounds.size.height / 2.0,
|
||||
1.0, 1.0);
|
||||
alert.popoverPresentationController.permittedArrowDirections = 0;
|
||||
}
|
||||
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)startLivePushWithProtocol:(AVStreamProtocol)protocol {
|
||||
SCLivePusherViewController *vc = [[SCLivePusherViewController alloc] init];
|
||||
vc.videoConfig = [[[AVConfigManager sharedManager] globalConfig] copy];
|
||||
|
||||
// 默认使用竖屏(用户可以在直播页面通过旋转按钮切换)
|
||||
vc.videoConfig.outputImageOrientation = UIInterfaceOrientationPortrait;
|
||||
|
||||
// 设置推流协议
|
||||
vc.protocol = protocol;
|
||||
|
||||
// 隐藏底部 TabBar
|
||||
vc.hidesBottomBarWhenPushed = YES;
|
||||
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
|
||||
NSLog(@"开始直播推流,协议: %@", AVStreamProtocolString(protocol));
|
||||
}
|
||||
|
||||
- (void)livePullTapped {
|
||||
// 弹出配置界面,让用户输入流信息
|
||||
SCPlayerConfigView *configView = [[SCPlayerConfigView alloc] init];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[configView showInViewController:self callback:^(SCPlayerConfig *config) {
|
||||
// 根据配置创建 AVLiveStreamModel
|
||||
AVLiveStreamModel *liveStream = [[AVLiveStreamModel alloc] init];
|
||||
liveStream.vhost = V_HOST;
|
||||
liveStream.app = APP_ID;
|
||||
liveStream.stream = config.streamId;
|
||||
liveStream.preview_image = @"";
|
||||
liveStream.duration = 0;
|
||||
liveStream.startTime = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
// 根据协议类型设置 playProtocol
|
||||
switch (config.protocol) {
|
||||
case SellyLiveMode_RTMP:
|
||||
liveStream.play_protocol = @"rtmp";
|
||||
break;
|
||||
case SellyLiveMode_RTC:
|
||||
liveStream.play_protocol = @"rtc";
|
||||
break;
|
||||
default:
|
||||
liveStream.play_protocol = @"rtmp";
|
||||
break;
|
||||
}
|
||||
|
||||
//这里仅做示例,实际上判断是直播还是点播
|
||||
if ([liveStream.stream.lowercaseString hasSuffix:@".mp4"]) {
|
||||
// 跳转到点播播放器
|
||||
SCVodVideoPlayerViewController *vc = [[SCVodVideoPlayerViewController alloc] initWithLiveStream:liveStream];
|
||||
vc.hidesBottomBarWhenPushed = YES;
|
||||
vc.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
[weakSelf.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
else {
|
||||
// 跳转到直播播放器
|
||||
SCLiveVideoPlayerViewController *vc = [[SCLiveVideoPlayerViewController alloc] initWithLiveStream:liveStream];
|
||||
vc.hidesBottomBarWhenPushed = YES;
|
||||
vc.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
[weakSelf.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation Support (强制只支持竖屏)
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES; // 允许旋转,但只支持竖屏
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
return UIInterfaceOrientationPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
19
Example/SellyCloudSDK/Controllers/Home/AVLiveStreamCell.h
Normal file
19
Example/SellyCloudSDK/Controllers/Home/AVLiveStreamCell.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// AVLiveStreamCell.h
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class AVLiveStreamModel;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVLiveStreamCell : UICollectionViewCell
|
||||
|
||||
/// 配置 Cell 显示内容
|
||||
- (void)configureWithModel:(AVLiveStreamModel *)model;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
182
Example/SellyCloudSDK/Controllers/Home/AVLiveStreamCell.m
Normal file
182
Example/SellyCloudSDK/Controllers/Home/AVLiveStreamCell.m
Normal file
@@ -0,0 +1,182 @@
|
||||
//
|
||||
// AVLiveStreamCell.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVLiveStreamCell.h"
|
||||
#import "AVLiveStreamModel.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <SDWebImage/SDWebImage.h>
|
||||
|
||||
@interface AVLiveStreamCell ()
|
||||
|
||||
@property (nonatomic, strong) UIImageView *thumbnailView;
|
||||
@property (nonatomic, strong) UIView *overlayView;
|
||||
@property (nonatomic, strong) UIImageView *playIcon;
|
||||
@property (nonatomic, strong) UILabel *durationLabel;
|
||||
@property (nonatomic, strong) UILabel *nameLabel;
|
||||
@property (nonatomic, strong) UILabel *liveLabel;
|
||||
@property (nonatomic, strong) UILabel *protocolLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AVLiveStreamCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
// 配置 Cell 背景
|
||||
self.contentView.backgroundColor = [UIColor secondarySystemBackgroundColor];
|
||||
self.contentView.layer.cornerRadius = 8;
|
||||
self.contentView.clipsToBounds = YES;
|
||||
|
||||
// 创建缩略图
|
||||
_thumbnailView = [[UIImageView alloc] init];
|
||||
_thumbnailView.backgroundColor = [UIColor systemGrayColor];
|
||||
_thumbnailView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_thumbnailView.clipsToBounds = YES;
|
||||
[self.contentView addSubview:_thumbnailView];
|
||||
|
||||
// 半透明遮罩层
|
||||
_overlayView = [[UIView alloc] init];
|
||||
_overlayView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2];
|
||||
[_thumbnailView addSubview:_overlayView];
|
||||
|
||||
// 播放图标
|
||||
_playIcon = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"play.circle.fill"]];
|
||||
_playIcon.tintColor = [UIColor whiteColor];
|
||||
_playIcon.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_playIcon.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
_playIcon.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
_playIcon.layer.shadowOpacity = 0.3;
|
||||
_playIcon.layer.shadowRadius = 4;
|
||||
[_thumbnailView addSubview:_playIcon];
|
||||
|
||||
// 直播时长标签
|
||||
_durationLabel = [[UILabel alloc] init];
|
||||
_durationLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightMedium];
|
||||
_durationLabel.textColor = [UIColor whiteColor];
|
||||
_durationLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
|
||||
_durationLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_durationLabel.layer.cornerRadius = 4;
|
||||
_durationLabel.clipsToBounds = YES;
|
||||
[_thumbnailView addSubview:_durationLabel];
|
||||
|
||||
// 流名称标签
|
||||
_nameLabel = [[UILabel alloc] init];
|
||||
_nameLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
|
||||
_nameLabel.textColor = [UIColor labelColor];
|
||||
_nameLabel.numberOfLines = 1;
|
||||
[self.contentView addSubview:_nameLabel];
|
||||
|
||||
// 底部信息容器
|
||||
UIView *infoContainerView = [[UIView alloc] init];
|
||||
[self.contentView addSubview:infoContainerView];
|
||||
|
||||
// 直播状态标签
|
||||
_liveLabel = [[UILabel alloc] init];
|
||||
_liveLabel.text = @"🔴 直播中";
|
||||
_liveLabel.font = [UIFont systemFontOfSize:11 weight:UIFontWeightMedium];
|
||||
_liveLabel.textColor = [UIColor systemRedColor];
|
||||
[infoContainerView addSubview:_liveLabel];
|
||||
|
||||
// 协议标签
|
||||
_protocolLabel = [[UILabel alloc] init];
|
||||
_protocolLabel.font = [UIFont systemFontOfSize:10 weight:UIFontWeightMedium];
|
||||
_protocolLabel.textColor = [UIColor whiteColor];
|
||||
_protocolLabel.backgroundColor = [UIColor systemBlueColor];
|
||||
_protocolLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_protocolLabel.layer.cornerRadius = 3;
|
||||
_protocolLabel.clipsToBounds = YES;
|
||||
[infoContainerView addSubview:_protocolLabel];
|
||||
|
||||
// 布局
|
||||
[_thumbnailView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.left.right.equalTo(self.contentView);
|
||||
// 使用较低优先级的宽高比约束 3:4,允许它在必要时被压缩
|
||||
make.height.equalTo(_thumbnailView.mas_width).multipliedBy(4.0 / 3.0).priority(750);
|
||||
}];
|
||||
|
||||
[_overlayView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(_thumbnailView);
|
||||
}];
|
||||
|
||||
[_playIcon mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(_thumbnailView);
|
||||
make.width.height.equalTo(@50);
|
||||
}];
|
||||
|
||||
[_durationLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.bottom.equalTo(_thumbnailView).offset(-8);
|
||||
make.right.equalTo(_thumbnailView).offset(-8);
|
||||
make.height.equalTo(@20);
|
||||
make.width.greaterThanOrEqualTo(@50);
|
||||
}];
|
||||
|
||||
[_nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(_thumbnailView.mas_bottom).offset(8);
|
||||
make.left.equalTo(self.contentView).offset(8);
|
||||
make.right.equalTo(self.contentView).offset(-8);
|
||||
}];
|
||||
|
||||
[infoContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(_nameLabel.mas_bottom).offset(4);
|
||||
make.left.equalTo(self.contentView).offset(8);
|
||||
make.right.lessThanOrEqualTo(self.contentView).offset(-8);
|
||||
make.bottom.lessThanOrEqualTo(self.contentView).offset(-8).priority(750);
|
||||
// 移除 height 的硬性约束,让它根据内容自适应
|
||||
}];
|
||||
|
||||
[_liveLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.top.bottom.equalTo(infoContainerView);
|
||||
}];
|
||||
|
||||
[_protocolLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(_liveLabel.mas_right).offset(8);
|
||||
make.centerY.equalTo(_liveLabel);
|
||||
make.height.equalTo(@16);
|
||||
make.width.greaterThanOrEqualTo(@40);
|
||||
make.right.lessThanOrEqualTo(infoContainerView);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)configureWithModel:(AVLiveStreamModel *)model {
|
||||
// 设置流名称
|
||||
_nameLabel.text = model.displayName;
|
||||
|
||||
// 设置时长
|
||||
_durationLabel.text = model.durationString;
|
||||
|
||||
// 设置协议标签
|
||||
_protocolLabel.text = model.play_protocol.uppercaseString;
|
||||
|
||||
// 使用 SDWebImage 加载预览图
|
||||
if (model.preview_image.length > 0) {
|
||||
NSURL *imageURL = [NSURL URLWithString:model.preview_image];
|
||||
|
||||
[_thumbnailView sd_setImageWithURL:imageURL];
|
||||
} else {
|
||||
// 没有预览图,清空图片
|
||||
_thumbnailView.image = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)prepareForReuse {
|
||||
[super prepareForReuse];
|
||||
|
||||
// 取消之前的图片加载任务
|
||||
[_thumbnailView sd_cancelCurrentImageLoad];
|
||||
|
||||
// 清空数据,防止复用时显示错误内容
|
||||
_thumbnailView.image = nil;
|
||||
_nameLabel.text = @"";
|
||||
_durationLabel.text = @"";
|
||||
_protocolLabel.text = @"";
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// AVNavigationController.h
|
||||
// AVDemo
|
||||
//
|
||||
// Created on 12/17/25.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 自定义导航控制器,用于转发屏幕方向控制权给顶层视图控制器
|
||||
@interface AVNavigationController : UINavigationController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// AVNavigationController.m
|
||||
// AVDemo
|
||||
//
|
||||
// Created on 12/17/25.
|
||||
//
|
||||
|
||||
#import "AVNavigationController.h"
|
||||
|
||||
@implementation AVNavigationController
|
||||
|
||||
#pragma mark - 屏幕方向控制
|
||||
|
||||
/// 是否支持自动旋转
|
||||
- (BOOL)shouldAutorotate {
|
||||
// 转发给顶层视图控制器
|
||||
return self.topViewController.shouldAutorotate;
|
||||
}
|
||||
|
||||
/// 支持的屏幕方向
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
// 转发给顶层视图控制器
|
||||
return self.topViewController.supportedInterfaceOrientations;
|
||||
}
|
||||
|
||||
/// 优先的屏幕方向
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
// 转发给顶层视图控制器
|
||||
return self.topViewController.preferredInterfaceOrientationForPresentation;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user