Initial clean commit
This commit is contained in:
26
Example/SellyCloudSDK/Controllers/AVLoginViewController.h
Normal file
26
Example/SellyCloudSDK/Controllers/AVLoginViewController.h
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// AVLoginViewController.h
|
||||
// AVDemo
|
||||
//
|
||||
// 登录视图控制器
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class AVLoginViewController;
|
||||
|
||||
@protocol AVLoginViewControllerDelegate <NSObject>
|
||||
@optional
|
||||
/// 登录成功回调
|
||||
- (void)loginViewControllerDidLogin:(AVLoginViewController *)controller;
|
||||
@end
|
||||
|
||||
@interface AVLoginViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id<AVLoginViewControllerDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
295
Example/SellyCloudSDK/Controllers/AVLoginViewController.m
Normal file
295
Example/SellyCloudSDK/Controllers/AVLoginViewController.m
Normal file
@@ -0,0 +1,295 @@
|
||||
//
|
||||
// AVLoginViewController.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVLoginViewController.h"
|
||||
#import "AVUserManager.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface AVLoginViewController () <UITextFieldDelegate>
|
||||
@property (nonatomic, strong) UIImageView *logoImageView;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UIView *usernameContainer;
|
||||
@property (nonatomic, strong) UITextField *usernameTextField;
|
||||
@property (nonatomic, strong) UIView *passwordContainer;
|
||||
@property (nonatomic, strong) UITextField *passwordTextField;
|
||||
@property (nonatomic, strong) UIButton *passwordToggleButton;
|
||||
@property (nonatomic, strong) UIButton *loginButton;
|
||||
@end
|
||||
|
||||
@implementation AVLoginViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor systemBackgroundColor];
|
||||
|
||||
[self setupUI];
|
||||
[self loadSavedUsername];
|
||||
|
||||
// 添加点击手势,点击空白处收起键盘
|
||||
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboard)];
|
||||
[self.view addGestureRecognizer:tapGesture];
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
// Logo 图标
|
||||
self.logoImageView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"play.rectangle.on.rectangle.fill"]];
|
||||
self.logoImageView.tintColor = [UIColor systemBlueColor];
|
||||
self.logoImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
[self.view addSubview:self.logoImageView];
|
||||
|
||||
[self.logoImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.view);
|
||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(80);
|
||||
make.width.height.equalTo(@100);
|
||||
}];
|
||||
|
||||
// 标题
|
||||
self.titleLabel = [[UILabel alloc] init];
|
||||
self.titleLabel.text = @"欢迎使用";
|
||||
self.titleLabel.font = [UIFont systemFontOfSize:28 weight:UIFontWeightBold];
|
||||
self.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[self.view addSubview:self.titleLabel];
|
||||
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.view);
|
||||
make.top.equalTo(self.logoImageView.mas_bottom).offset(24);
|
||||
}];
|
||||
|
||||
// 用户名输入框容器
|
||||
self.usernameContainer = [[UIView alloc] init];
|
||||
self.usernameContainer.backgroundColor = [UIColor secondarySystemBackgroundColor];
|
||||
self.usernameContainer.layer.cornerRadius = 12;
|
||||
[self.view addSubview:self.usernameContainer];
|
||||
|
||||
[self.usernameContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.view);
|
||||
make.top.equalTo(self.titleLabel.mas_bottom).offset(60);
|
||||
make.left.equalTo(self.view).offset(32);
|
||||
make.right.equalTo(self.view).offset(-32);
|
||||
make.height.equalTo(@50);
|
||||
}];
|
||||
|
||||
// 用户名图标
|
||||
UIImageView *usernameIcon = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"person.fill"]];
|
||||
usernameIcon.tintColor = [UIColor systemGrayColor];
|
||||
usernameIcon.contentMode = UIViewContentModeScaleAspectFit;
|
||||
[self.usernameContainer addSubview:usernameIcon];
|
||||
|
||||
[usernameIcon mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.usernameContainer).offset(16);
|
||||
make.centerY.equalTo(self.usernameContainer);
|
||||
make.width.height.equalTo(@20);
|
||||
}];
|
||||
|
||||
// 用户名输入框
|
||||
self.usernameTextField = [[UITextField alloc] init];
|
||||
self.usernameTextField.placeholder = @"请输入用户名";
|
||||
self.usernameTextField.font = [UIFont systemFontOfSize:16];
|
||||
self.usernameTextField.returnKeyType = UIReturnKeyNext;
|
||||
self.usernameTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.usernameTextField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.usernameTextField.delegate = self;
|
||||
[self.usernameContainer addSubview:self.usernameTextField];
|
||||
|
||||
[self.usernameTextField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(usernameIcon.mas_right).offset(12);
|
||||
make.right.equalTo(self.usernameContainer).offset(-16);
|
||||
make.centerY.equalTo(self.usernameContainer);
|
||||
}];
|
||||
|
||||
// 密码输入框容器
|
||||
self.passwordContainer = [[UIView alloc] init];
|
||||
self.passwordContainer.backgroundColor = [UIColor secondarySystemBackgroundColor];
|
||||
self.passwordContainer.layer.cornerRadius = 12;
|
||||
[self.view addSubview:self.passwordContainer];
|
||||
|
||||
[self.passwordContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.view);
|
||||
make.top.equalTo(self.usernameContainer.mas_bottom).offset(16);
|
||||
make.left.equalTo(self.view).offset(32);
|
||||
make.right.equalTo(self.view).offset(-32);
|
||||
make.height.equalTo(@50);
|
||||
}];
|
||||
|
||||
// 密码图标
|
||||
UIImageView *passwordIcon = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"lock.fill"]];
|
||||
passwordIcon.tintColor = [UIColor systemGrayColor];
|
||||
passwordIcon.contentMode = UIViewContentModeScaleAspectFit;
|
||||
[self.passwordContainer addSubview:passwordIcon];
|
||||
|
||||
[passwordIcon mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.passwordContainer).offset(16);
|
||||
make.centerY.equalTo(self.passwordContainer);
|
||||
make.width.height.equalTo(@20);
|
||||
}];
|
||||
|
||||
// 密码输入框
|
||||
self.passwordTextField = [[UITextField alloc] init];
|
||||
self.passwordTextField.placeholder = @"请输入密码";
|
||||
self.passwordTextField.font = [UIFont systemFontOfSize:16];
|
||||
self.passwordTextField.secureTextEntry = YES;
|
||||
self.passwordTextField.returnKeyType = UIReturnKeyDone;
|
||||
self.passwordTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.passwordTextField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.passwordTextField.delegate = self;
|
||||
[self.passwordContainer addSubview:self.passwordTextField];
|
||||
|
||||
// 密码显示/隐藏切换按钮
|
||||
self.passwordToggleButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[self.passwordToggleButton setImage:[UIImage systemImageNamed:@"eye.slash.fill"] forState:UIControlStateNormal];
|
||||
[self.passwordToggleButton setImage:[UIImage systemImageNamed:@"eye.fill"] forState:UIControlStateSelected];
|
||||
self.passwordToggleButton.tintColor = [UIColor systemGrayColor];
|
||||
[self.passwordToggleButton addTarget:self action:@selector(togglePasswordVisibility) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.passwordContainer addSubview:self.passwordToggleButton];
|
||||
|
||||
[self.passwordToggleButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.passwordContainer).offset(-16);
|
||||
make.centerY.equalTo(self.passwordContainer);
|
||||
make.width.height.equalTo(@24);
|
||||
}];
|
||||
|
||||
[self.passwordTextField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(passwordIcon.mas_right).offset(12);
|
||||
make.right.equalTo(self.passwordToggleButton.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.passwordContainer);
|
||||
}];
|
||||
|
||||
// 登录按钮
|
||||
self.loginButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[self.loginButton setTitle:@"登录" forState:UIControlStateNormal];
|
||||
self.loginButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
||||
self.loginButton.backgroundColor = [UIColor systemBlueColor];
|
||||
[self.loginButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
self.loginButton.layer.cornerRadius = 12;
|
||||
[self.loginButton addTarget:self action:@selector(loginButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:self.loginButton];
|
||||
|
||||
[self.loginButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.view);
|
||||
make.top.equalTo(self.passwordContainer.mas_bottom).offset(32);
|
||||
make.left.equalTo(self.view).offset(32);
|
||||
make.right.equalTo(self.view).offset(-32);
|
||||
make.height.equalTo(@50);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)loadSavedUsername {
|
||||
// 从 AVUserManager 获取上次登录的用户名
|
||||
NSString *savedUsername = [AVUserManager sharedManager].currentUsername;
|
||||
|
||||
if (savedUsername.length > 0) {
|
||||
self.usernameTextField.text = savedUsername;
|
||||
NSLog(@"✅ 自动填充用户名: %@", savedUsername);
|
||||
|
||||
// 如果有保存的用户名,自动聚焦到密码输入框
|
||||
[self.passwordTextField becomeFirstResponder];
|
||||
} else {
|
||||
// 如果没有保存的用户名,聚焦到用户名输入框
|
||||
[self.usernameTextField becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)loginButtonTapped {
|
||||
[self dismissKeyboard];
|
||||
|
||||
NSString *username = self.usernameTextField.text;
|
||||
NSString *password = self.passwordTextField.text;
|
||||
|
||||
// 验证输入
|
||||
if (username.length == 0) {
|
||||
[self showAlertWithMessage:@"请输入用户名"];
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length == 0) {
|
||||
[self showAlertWithMessage:@"请输入密码"];
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用登录按钮,防止重复提交
|
||||
self.loginButton.enabled = NO;
|
||||
[self.loginButton setTitle:@"登录中..." forState:UIControlStateNormal];
|
||||
self.loginButton.alpha = 0.6;
|
||||
|
||||
// 执行登录(异步)
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[[AVUserManager sharedManager] loginWithUsername:username password:password completion:^(BOOL success, NSString * _Nullable errorMessage) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
|
||||
// 恢复登录按钮状态
|
||||
strongSelf.loginButton.enabled = YES;
|
||||
[strongSelf.loginButton setTitle:@"登录" forState:UIControlStateNormal];
|
||||
strongSelf.loginButton.alpha = 1.0;
|
||||
|
||||
if (success) {
|
||||
// 登录成功,通知代理
|
||||
NSLog(@"✅ 登录验证成功");
|
||||
if ([strongSelf.delegate respondsToSelector:@selector(loginViewControllerDidLogin:)]) {
|
||||
[strongSelf.delegate loginViewControllerDidLogin:strongSelf];
|
||||
}
|
||||
} else {
|
||||
// 登录失败,显示错误信息
|
||||
NSLog(@"❌ 登录验证失败: %@", errorMessage);
|
||||
[strongSelf showAlertWithMessage:errorMessage ?: @"登录失败,请重试"];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissKeyboard {
|
||||
[self.view endEditing:YES];
|
||||
}
|
||||
|
||||
- (void)togglePasswordVisibility {
|
||||
// 切换密码的可见性
|
||||
self.passwordTextField.secureTextEntry = !self.passwordTextField.secureTextEntry;
|
||||
|
||||
// 切换按钮状态
|
||||
self.passwordToggleButton.selected = !self.passwordToggleButton.selected;
|
||||
|
||||
// 保持光标位置(修复切换时光标跳到开头的问题)
|
||||
NSString *currentText = self.passwordTextField.text;
|
||||
self.passwordTextField.text = @"";
|
||||
self.passwordTextField.text = currentText;
|
||||
}
|
||||
|
||||
- (void)showAlertWithMessage:(NSString *)message {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - UITextFieldDelegate
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
if (textField == self.usernameTextField) {
|
||||
// 用户名输入框按回车,跳到密码输入框
|
||||
[self.passwordTextField becomeFirstResponder];
|
||||
} else if (textField == self.passwordTextField) {
|
||||
// 密码输入框按回车,执行登录
|
||||
[self loginButtonTapped];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Orientation Support
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
return UIInterfaceOrientationPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
14
Example/SellyCloudSDK/Controllers/AVTabBarController.h
Normal file
14
Example/SellyCloudSDK/Controllers/AVTabBarController.h
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// AVTabBarController.h
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVTabBarController : UITabBarController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
122
Example/SellyCloudSDK/Controllers/AVTabBarController.m
Normal file
122
Example/SellyCloudSDK/Controllers/AVTabBarController.m
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// AVTabBarController.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVTabBarController.h"
|
||||
#import "AVHomeViewController.h"
|
||||
#import "AVCallViewController.h"
|
||||
#import "AVVodListViewController.h"
|
||||
#import "AVSettingsViewController.h"
|
||||
#import "AVLoginViewController.h"
|
||||
#import "AVUserManager.h"
|
||||
|
||||
@interface AVTabBarController () <AVLoginViewControllerDelegate>
|
||||
@end
|
||||
|
||||
@implementation AVTabBarController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
// Create Home tab
|
||||
AVHomeViewController *homeVC = [[AVHomeViewController alloc] init];
|
||||
UINavigationController *homeNav = [[UINavigationController alloc] initWithRootViewController:homeVC];
|
||||
homeNav.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"首页"
|
||||
image:[UIImage systemImageNamed:@"house"]
|
||||
selectedImage:[UIImage systemImageNamed:@"house.fill"]];
|
||||
|
||||
// Create VOD tab
|
||||
AVVodListViewController *vodVC = [[AVVodListViewController alloc] init];
|
||||
UINavigationController *vodNav = [[UINavigationController alloc] initWithRootViewController:vodVC];
|
||||
vodNav.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"点播"
|
||||
image:[UIImage systemImageNamed:@"play.rectangle"]
|
||||
selectedImage:[UIImage systemImageNamed:@"play.rectangle.fill"]];
|
||||
|
||||
// Create Call tab
|
||||
AVCallViewController *callVC = [[AVCallViewController alloc] init];
|
||||
UINavigationController *callNav = [[UINavigationController alloc] initWithRootViewController:callVC];
|
||||
callNav.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"通话"
|
||||
image:[UIImage systemImageNamed:@"phone"]
|
||||
selectedImage:[UIImage systemImageNamed:@"phone.fill"]];
|
||||
|
||||
// Create Settings tab
|
||||
AVSettingsViewController *settingsVC = [[AVSettingsViewController alloc] init];
|
||||
UINavigationController *settingsNav = [[UINavigationController alloc] initWithRootViewController:settingsVC];
|
||||
settingsNav.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"设置"
|
||||
image:[UIImage systemImageNamed:@"gearshape"]
|
||||
selectedImage:[UIImage systemImageNamed:@"gearshape.fill"]];
|
||||
|
||||
// Set view controllers
|
||||
self.viewControllers = @[homeNav, vodNav, callNav, settingsNav];
|
||||
|
||||
// Customize tab bar appearance
|
||||
self.tabBar.tintColor = [UIColor systemBlueColor];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
// 检查登录状态
|
||||
[self checkLoginStatus];
|
||||
}
|
||||
|
||||
#pragma mark - Login Management
|
||||
|
||||
- (void)checkLoginStatus {
|
||||
// 如果未登录,显示登录页面
|
||||
if (![AVUserManager sharedManager].isLoggedIn) {
|
||||
[self showLoginViewController];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showLoginViewController {
|
||||
AVLoginViewController *loginVC = [[AVLoginViewController alloc] init];
|
||||
loginVC.delegate = self;
|
||||
loginVC.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
|
||||
// 确保不会重复弹出登录页面
|
||||
if (self.presentedViewController == nil) {
|
||||
[self presentViewController:loginVC animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - AVLoginViewControllerDelegate
|
||||
|
||||
- (void)loginViewControllerDidLogin:(AVLoginViewController *)controller {
|
||||
// 登录成功,关闭登录页面
|
||||
[controller dismissViewControllerAnimated:YES completion:^{
|
||||
NSLog(@"✅ 登录成功,已进入应用");
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation Support (将方向决策交给顶层 ViewController)
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
// 返回当前显示的 NavigationController 的顶层 VC 的设置
|
||||
UINavigationController *nav = (UINavigationController *)self.selectedViewController;
|
||||
if ([nav isKindOfClass:[UINavigationController class]]) {
|
||||
return [nav.topViewController shouldAutorotate];
|
||||
}
|
||||
return [self.selectedViewController shouldAutorotate];
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
// 返回当前显示的 NavigationController 的顶层 VC 的设置
|
||||
UINavigationController *nav = (UINavigationController *)self.selectedViewController;
|
||||
if ([nav isKindOfClass:[UINavigationController class]]) {
|
||||
return [nav.topViewController supportedInterfaceOrientations];
|
||||
}
|
||||
return [self.selectedViewController supportedInterfaceOrientations];
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
// 返回当前显示的 NavigationController 的顶层 VC 的设置
|
||||
UINavigationController *nav = (UINavigationController *)self.selectedViewController;
|
||||
if ([nav isKindOfClass:[UINavigationController class]]) {
|
||||
return [nav.topViewController preferredInterfaceOrientationForPresentation];
|
||||
}
|
||||
return [self.selectedViewController preferredInterfaceOrientationForPresentation];
|
||||
}
|
||||
|
||||
@end
|
||||
36
Example/SellyCloudSDK/Controllers/AVUserManager.h
Normal file
36
Example/SellyCloudSDK/Controllers/AVUserManager.h
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// AVUserManager.h
|
||||
// AVDemo
|
||||
//
|
||||
// 用户管理类 - 管理用户登录状态
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVUserManager : NSObject
|
||||
|
||||
/// 单例
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
/// 检查用户是否已登录
|
||||
@property (nonatomic, assign, readonly) BOOL isLoggedIn;
|
||||
|
||||
/// 当前用户名(可选)
|
||||
@property (nonatomic, copy, nullable) NSString *currentUsername;
|
||||
|
||||
/// 登录(异步,带回调)
|
||||
/// @param username 用户名
|
||||
/// @param password 密码
|
||||
/// @param completion 完成回调,success 表示是否成功,errorMessage 为错误信息(成功时为 nil)
|
||||
- (void)loginWithUsername:(NSString *)username
|
||||
password:(NSString *)password
|
||||
completion:(void (^)(BOOL success, NSString * _Nullable errorMessage))completion;
|
||||
|
||||
/// 登出
|
||||
- (void)logout;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
151
Example/SellyCloudSDK/Controllers/AVUserManager.m
Normal file
151
Example/SellyCloudSDK/Controllers/AVUserManager.m
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// AVUserManager.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVUserManager.h"
|
||||
#import <AFNetworking/AFNetworking.h>
|
||||
|
||||
static NSString * const kUserLoggedInKey = @"AVUserLoggedIn";
|
||||
static NSString * const kUserUsernameKey = @"AVUserUsername";
|
||||
|
||||
@interface AVUserManager ()
|
||||
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;
|
||||
@end
|
||||
|
||||
@implementation AVUserManager
|
||||
|
||||
+ (instancetype)sharedManager {
|
||||
static AVUserManager *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// 从 UserDefaults 读取登录状态
|
||||
_isLoggedIn = [[NSUserDefaults standardUserDefaults] boolForKey:kUserLoggedInKey];
|
||||
_currentUsername = [[NSUserDefaults standardUserDefaults] stringForKey:kUserUsernameKey];
|
||||
|
||||
// 初始化网络请求管理器
|
||||
self.sessionManager = [AFHTTPSessionManager manager];
|
||||
self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
|
||||
self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
|
||||
self.sessionManager.requestSerializer.timeoutInterval = 10;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loginWithUsername:(NSString *)username
|
||||
password:(NSString *)password
|
||||
completion:(void (^)(BOOL success, NSString * _Nullable errorMessage))completion {
|
||||
|
||||
// 基本参数验证
|
||||
if (username.length == 0) {
|
||||
if (completion) {
|
||||
completion(NO, @"用户名不能为空");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length == 0) {
|
||||
if (completion) {
|
||||
completion(NO, @"密码不能为空");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 登录 API 端点
|
||||
NSString *loginURL = @"http://rtmp.sellycloud.io:8089/live/sdk/demo/login";
|
||||
|
||||
// 请求参数
|
||||
NSDictionary *parameters = @{
|
||||
@"username": username,
|
||||
@"password": password
|
||||
};
|
||||
|
||||
NSLog(@"🚀 开始登录请求: %@", username);
|
||||
|
||||
// 发起 POST 请求
|
||||
[self.sessionManager POST:loginURL parameters:parameters headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
|
||||
// 登录成功
|
||||
NSLog(@"✅ 登录成功,服务器响应: %@", responseObject);
|
||||
|
||||
// 保存登录状态
|
||||
[self saveLoginStateWithUsername:username];
|
||||
|
||||
// 可以根据需要保存服务器返回的其他信息(如 token、userId 等)
|
||||
// 例如:
|
||||
// if (responseObject[@"token"]) {
|
||||
// [[NSUserDefaults standardUserDefaults] setObject:responseObject[@"token"] forKey:@"AVUserToken"];
|
||||
// }
|
||||
|
||||
// 回调成功
|
||||
if (completion) {
|
||||
completion(YES, nil);
|
||||
}
|
||||
|
||||
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
|
||||
// 登录失败
|
||||
NSLog(@"❌ 登录失败: %@", error.localizedDescription);
|
||||
|
||||
NSString *errorMessage = @"登录失败,请检查用户名和密码";
|
||||
|
||||
// 尝试获取 HTTP 状态码
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
|
||||
if (httpResponse) {
|
||||
NSLog(@"❌ HTTP 状态码: %ld", (long)httpResponse.statusCode);
|
||||
}
|
||||
|
||||
// 尝试解析服务器返回的错误信息
|
||||
if (error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]) {
|
||||
NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
|
||||
NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData options:0 error:nil];
|
||||
|
||||
NSLog(@"❌ 服务器错误响应: %@", errorDict);
|
||||
|
||||
// 尝试多种可能的错误信息字段
|
||||
if (errorDict[@"message"]) {
|
||||
errorMessage = errorDict[@"message"];
|
||||
} else if (errorDict[@"error"]) {
|
||||
errorMessage = errorDict[@"error"];
|
||||
} else if (errorDict[@"msg"]) {
|
||||
errorMessage = errorDict[@"msg"];
|
||||
}
|
||||
} else {
|
||||
// 网络错误
|
||||
errorMessage = [NSString stringWithFormat:@"网络请求失败: %@", error.localizedDescription];
|
||||
}
|
||||
|
||||
// 回调失败
|
||||
if (completion) {
|
||||
completion(NO, errorMessage);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)saveLoginStateWithUsername:(NSString *)username {
|
||||
_isLoggedIn = YES;
|
||||
_currentUsername = username;
|
||||
|
||||
// 持久化保存
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:kUserLoggedInKey];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:username forKey:kUserUsernameKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (void)logout {
|
||||
_isLoggedIn = NO;
|
||||
|
||||
// 清除持久化数据
|
||||
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:kUserLoggedInKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
|
||||
NSLog(@"✅ 用户已登出");
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
399
Example/SellyCloudSDK/Controllers/Home/AVHomeViewController.m
Normal file
399
Example/SellyCloudSDK/Controllers/Home/AVHomeViewController.m
Normal file
@@ -0,0 +1,399 @@
|
||||
//
|
||||
// AVHomeViewController.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVHomeViewController.h"
|
||||
#import <Kiwi/Kiwi.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];
|
||||
//初始化洋葱盾相关,业务层调用 Kiwi 后将地址传给 SDK
|
||||
[Kiwi Init:kiwiAppkey];
|
||||
char ip[40] = {0};
|
||||
char port[40] = {0};
|
||||
[Kiwi ServerToLocal:"123" :ip :sizeof(ip) :port :sizeof(port)];
|
||||
NSString *proxyAddress = [NSString stringWithFormat:@"http://%s:%s", ip, port];
|
||||
[SellyCloudManager setProxyAddress:proxyAddress];
|
||||
|
||||
// 生成随机 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
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// AVSettingsViewController.h
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVSettingsViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,282 @@
|
||||
//
|
||||
// AVSettingsViewController.m
|
||||
// AVDemo
|
||||
//
|
||||
|
||||
#import "AVSettingsViewController.h"
|
||||
#import "AVConfigManager.h"
|
||||
#import "AVConstants.h"
|
||||
#import "AVUserManager.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import "UIView+SellyCloud.h"
|
||||
|
||||
@interface AVSettingsViewController () <UITextFieldDelegate>
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic, strong) UIView *contentView;
|
||||
|
||||
@property (nonatomic, strong) UITextField *nicknameField;
|
||||
@property (nonatomic, strong) UISegmentedControl *resolutionSegment;
|
||||
@property (nonatomic, strong) UITextField *fpsField;
|
||||
@property (nonatomic, strong) UITextField *maxBitrateField;
|
||||
@property (nonatomic, strong) UITextField *minBitrateField;
|
||||
@end
|
||||
|
||||
@implementation AVSettingsViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor systemBackgroundColor];
|
||||
self.title = @"设置";
|
||||
|
||||
// 添加右上角提交日志按钮 - 延迟设置以避免约束冲突
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIBarButtonItem *uploadLogButton = [[UIBarButtonItem alloc] initWithTitle:@"提交日志"
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(uploadLogButtonTapped)];
|
||||
self.navigationItem.rightBarButtonItem = uploadLogButton;
|
||||
});
|
||||
|
||||
[self setupUI];
|
||||
[self loadConfig];
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
_scrollView = [[UIScrollView alloc] init];
|
||||
[self.view addSubview:_scrollView];
|
||||
|
||||
_contentView = [[UIView alloc] init];
|
||||
[_scrollView addSubview:_contentView];
|
||||
|
||||
[_scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
|
||||
make.left.right.bottom.equalTo(self.view);
|
||||
}];
|
||||
|
||||
[_contentView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(_scrollView);
|
||||
make.width.equalTo(_scrollView);
|
||||
}];
|
||||
|
||||
CGFloat topOffset = 20;
|
||||
CGFloat spacing = 30;
|
||||
|
||||
// Nickname
|
||||
topOffset = [self addLabel:@"用户昵称" atOffset:topOffset];
|
||||
_nicknameField = [self addTextField:@"请输入昵称" atOffset:topOffset];
|
||||
topOffset += 40 + spacing;
|
||||
|
||||
// Resolution
|
||||
topOffset = [self addLabel:@"分辨率" atOffset:topOffset];
|
||||
_resolutionSegment = [[UISegmentedControl alloc] initWithItems:@[@"360p", @"480p", @"540p", @"720p"]];
|
||||
[_contentView addSubview:_resolutionSegment];
|
||||
|
||||
[_resolutionSegment mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(_contentView).offset(20);
|
||||
make.right.equalTo(_contentView).offset(-20);
|
||||
make.top.equalTo(_contentView).offset(topOffset);
|
||||
make.height.equalTo(@32);
|
||||
}];
|
||||
topOffset += 32 + spacing;
|
||||
|
||||
// FPS
|
||||
topOffset = [self addLabel:@"帧率 (FPS)" atOffset:topOffset];
|
||||
_fpsField = [self addTextField:@"例如: 30" atOffset:topOffset];
|
||||
_fpsField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
topOffset += 40 + spacing;
|
||||
|
||||
// Max bitrate
|
||||
topOffset = [self addLabel:@"最大码率 (kbps)" atOffset:topOffset];
|
||||
_maxBitrateField = [self addTextField:@"例如: 2000" atOffset:topOffset];
|
||||
_maxBitrateField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
topOffset += 40 + spacing;
|
||||
|
||||
// Min bitrate
|
||||
topOffset = [self addLabel:@"最小码率 (kbps)" atOffset:topOffset];
|
||||
_minBitrateField = [self addTextField:@"例如: 500" atOffset:topOffset];
|
||||
_minBitrateField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
topOffset += 40 + spacing;
|
||||
|
||||
// Save button
|
||||
UIButton *saveButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[saveButton setTitle:@"保存设置" forState:UIControlStateNormal];
|
||||
saveButton.backgroundColor = [UIColor systemBlueColor];
|
||||
[saveButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
saveButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
|
||||
saveButton.layer.cornerRadius = 10;
|
||||
[saveButton addTarget:self action:@selector(saveConfig) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_contentView addSubview:saveButton];
|
||||
|
||||
[saveButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(_contentView).offset(20);
|
||||
make.right.equalTo(_contentView).offset(-20);
|
||||
make.top.equalTo(_contentView).offset(topOffset);
|
||||
make.height.equalTo(@50);
|
||||
}];
|
||||
topOffset += 50 + 20;
|
||||
|
||||
// Logout button
|
||||
UIButton *logoutButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[logoutButton setTitle:@"退出登录" forState:UIControlStateNormal];
|
||||
logoutButton.backgroundColor = [UIColor systemRedColor];
|
||||
[logoutButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
logoutButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
|
||||
logoutButton.layer.cornerRadius = 10;
|
||||
[logoutButton addTarget:self action:@selector(logoutButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_contentView addSubview:logoutButton];
|
||||
|
||||
[logoutButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(_contentView).offset(20);
|
||||
make.right.equalTo(_contentView).offset(-20);
|
||||
make.top.equalTo(_contentView).offset(topOffset);
|
||||
make.height.equalTo(@50);
|
||||
}];
|
||||
topOffset += 50 + 20;
|
||||
|
||||
// 设置 contentView 的高度
|
||||
[_contentView mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
make.height.equalTo(@(topOffset));
|
||||
}];
|
||||
|
||||
// Dismiss keyboard on tap
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboard)];
|
||||
[self.view addGestureRecognizer:tap];
|
||||
}
|
||||
|
||||
- (CGFloat)addLabel:(NSString *)text atOffset:(CGFloat)offset {
|
||||
UILabel *label = [[UILabel alloc] init];
|
||||
label.text = text;
|
||||
label.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
|
||||
label.textColor = [UIColor labelColor];
|
||||
[_contentView addSubview:label];
|
||||
|
||||
[label mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(_contentView).offset(20);
|
||||
make.top.equalTo(_contentView).offset(offset);
|
||||
}];
|
||||
|
||||
return offset + 25;
|
||||
}
|
||||
|
||||
- (UITextField *)addTextField:(NSString *)placeholder atOffset:(CGFloat)offset {
|
||||
UITextField *textField = [[UITextField alloc] init];
|
||||
textField.placeholder = placeholder;
|
||||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||||
textField.delegate = self;
|
||||
[_contentView addSubview:textField];
|
||||
|
||||
[textField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(_contentView).offset(20);
|
||||
make.right.equalTo(_contentView).offset(-20);
|
||||
make.top.equalTo(_contentView).offset(offset);
|
||||
make.height.equalTo(@40);
|
||||
}];
|
||||
|
||||
return textField;
|
||||
}
|
||||
|
||||
- (void)loadConfig {
|
||||
AVVideoConfiguration *config = [AVConfigManager sharedManager].globalConfig;
|
||||
|
||||
_nicknameField.text = config.nickname;
|
||||
_resolutionSegment.selectedSegmentIndex = [config currentResolution];
|
||||
_fpsField.text = [NSString stringWithFormat:@"%ld", (long)config.videoFrameRate];
|
||||
_maxBitrateField.text = [NSString stringWithFormat:@"%ld", (long)(config.videoBitRate / 1000)];
|
||||
_minBitrateField.text = [NSString stringWithFormat:@"%ld", (long)(config.videoMinBitRate / 1000)];
|
||||
}
|
||||
|
||||
- (void)saveConfig {
|
||||
AVVideoConfiguration *config = [AVConfigManager sharedManager].globalConfig;
|
||||
|
||||
config.nickname = _nicknameField.text.length > 0 ? _nicknameField.text : [AVConfigManager generateRandomNickname];
|
||||
[config setResolution:_resolutionSegment.selectedSegmentIndex];
|
||||
|
||||
NSInteger fps = [_fpsField.text integerValue] > 0 ? [_fpsField.text integerValue] : 30;
|
||||
config.videoFrameRate = fps;
|
||||
config.videoMaxKeyframeInterval = fps * 2;
|
||||
|
||||
NSInteger maxBitrate = [_maxBitrateField.text integerValue] > 0 ? [_maxBitrateField.text integerValue] : 2000;
|
||||
config.videoBitRate = maxBitrate * 1000; // Convert kbps to bps
|
||||
|
||||
NSInteger minBitrate = [_minBitrateField.text integerValue] > 0 ? [_minBitrateField.text integerValue] : 500;
|
||||
config.videoMinBitRate = minBitrate * 1000; // Convert kbps to bps
|
||||
|
||||
[[AVConfigManager sharedManager] saveConfig];
|
||||
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"成功" message:@"设置已保存" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)logoutButtonTapped {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"确认退出"
|
||||
message:@"确定要退出登录吗?"
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
[alert addAction:cancelAction];
|
||||
|
||||
UIAlertAction *logoutAction = [UIAlertAction actionWithTitle:@"退出"
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
// 执行登出
|
||||
[[AVUserManager sharedManager] logout];
|
||||
|
||||
// 返回到登录页面
|
||||
// 通过 TabBarController 触发登录检查
|
||||
if ([self.tabBarController respondsToSelector:@selector(checkLoginStatus)]) {
|
||||
[self.tabBarController performSelector:@selector(checkLoginStatus)];
|
||||
}
|
||||
|
||||
NSLog(@"✅ 已退出登录");
|
||||
}];
|
||||
[alert addAction:logoutAction];
|
||||
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)uploadLogButtonTapped {
|
||||
// 调用 SellyCloudManager 的 uploadLog 方法
|
||||
[SellyCloudManager uploadLog:^(NSString * _Nullable url, NSError * _Nullable error) {
|
||||
UIAlertController *resultAlert;
|
||||
if (error) {
|
||||
// 上传失败
|
||||
resultAlert = [UIAlertController alertControllerWithTitle:@"上传失败"
|
||||
message:[NSString stringWithFormat:@"日志上传失败: %@", error.localizedDescription]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[resultAlert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||
} else {
|
||||
// 上传成功
|
||||
NSString *message = url ? [NSString stringWithFormat:@"日志已成功上传\n地址: %@", url] : @"日志已成功上传";
|
||||
resultAlert = [UIAlertController alertControllerWithTitle:@"上传成功"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
NSLog(@"###日志url == %@",url);
|
||||
|
||||
// 点击确定后将 URL 复制到粘贴板
|
||||
UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确定"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
if (url.length > 0) {
|
||||
UIPasteboard.generalPasteboard.string = url;
|
||||
[self.view showToast:@"日志 URL 已复制到粘贴板"];
|
||||
}
|
||||
}];
|
||||
[resultAlert addAction:confirmAction];
|
||||
}
|
||||
[self presentViewController:resultAlert animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissKeyboard {
|
||||
[self.view endEditing:YES];
|
||||
}
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
[textField resignFirstResponder];
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// UINavigationController+Orientation.h
|
||||
// AVDemo
|
||||
//
|
||||
// 让导航控制器将方向决策交给顶层 ViewController
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UINavigationController (Orientation)
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// UINavigationController+Orientation.m
|
||||
// AVDemo
|
||||
//
|
||||
// 让导航控制器将方向决策交给顶层 ViewController
|
||||
//
|
||||
|
||||
#import "UINavigationController+Orientation.h"
|
||||
|
||||
@implementation UINavigationController (Orientation)
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
// 将决策权交给顶层 ViewController
|
||||
return [self.topViewController shouldAutorotate];
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
// 将决策权交给顶层 ViewController
|
||||
return [self.topViewController supportedInterfaceOrientations];
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
// 将决策权交给顶层 ViewController
|
||||
return [self.topViewController preferredInterfaceOrientationForPresentation];
|
||||
}
|
||||
|
||||
@end
|
||||
18
Example/SellyCloudSDK/Controllers/VOD/AVVodItemCell.h
Normal file
18
Example/SellyCloudSDK/Controllers/VOD/AVVodItemCell.h
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// AVVodItemCell.h
|
||||
// SellyCloudSDK_Example
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class AVVodItemModel;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVVodItemCell : UICollectionViewCell
|
||||
|
||||
- (void)configureWithModel:(AVVodItemModel *)model;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
124
Example/SellyCloudSDK/Controllers/VOD/AVVodItemCell.m
Normal file
124
Example/SellyCloudSDK/Controllers/VOD/AVVodItemCell.m
Normal file
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// AVVodItemCell.m
|
||||
// SellyCloudSDK_Example
|
||||
//
|
||||
|
||||
#import "AVVodItemCell.h"
|
||||
#import "AVVodItemModel.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <SDWebImage/SDWebImage.h>
|
||||
|
||||
@interface AVVodItemCell ()
|
||||
|
||||
@property (nonatomic, strong) UIImageView *thumbnailView;
|
||||
@property (nonatomic, strong) UIView *overlayView;
|
||||
@property (nonatomic, strong) UIImageView *playIcon;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *typeLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AVVodItemCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
self.contentView.backgroundColor = [UIColor secondarySystemBackgroundColor];
|
||||
self.contentView.layer.cornerRadius = 8;
|
||||
self.contentView.clipsToBounds = YES;
|
||||
|
||||
// Thumbnail
|
||||
_thumbnailView = [[UIImageView alloc] init];
|
||||
_thumbnailView.backgroundColor = [UIColor systemGray4Color];
|
||||
_thumbnailView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_thumbnailView.clipsToBounds = YES;
|
||||
[self.contentView addSubview:_thumbnailView];
|
||||
|
||||
// Overlay
|
||||
_overlayView = [[UIView alloc] init];
|
||||
_overlayView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2];
|
||||
[_thumbnailView addSubview:_overlayView];
|
||||
|
||||
// Play icon
|
||||
_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];
|
||||
|
||||
// Type label (badge on thumbnail)
|
||||
_typeLabel = [[UILabel alloc] init];
|
||||
_typeLabel.font = [UIFont systemFontOfSize:10 weight:UIFontWeightMedium];
|
||||
_typeLabel.textColor = [UIColor whiteColor];
|
||||
_typeLabel.backgroundColor = [UIColor systemBlueColor];
|
||||
_typeLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_typeLabel.layer.cornerRadius = 3;
|
||||
_typeLabel.clipsToBounds = YES;
|
||||
[_thumbnailView addSubview:_typeLabel];
|
||||
|
||||
// Title label
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
|
||||
_titleLabel.textColor = [UIColor labelColor];
|
||||
_titleLabel.numberOfLines = 2;
|
||||
[self.contentView addSubview:_titleLabel];
|
||||
|
||||
// Layout
|
||||
[_thumbnailView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.left.right.equalTo(self.contentView);
|
||||
make.height.equalTo(_thumbnailView.mas_width).multipliedBy(9.0 / 16.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(@44);
|
||||
}];
|
||||
|
||||
[_typeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(_thumbnailView).offset(8);
|
||||
make.left.equalTo(_thumbnailView).offset(8);
|
||||
make.height.equalTo(@18);
|
||||
make.width.greaterThanOrEqualTo(@36);
|
||||
}];
|
||||
|
||||
[_titleLabel 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);
|
||||
make.bottom.lessThanOrEqualTo(self.contentView).offset(-8).priority(750);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)configureWithModel:(AVVodItemModel *)model {
|
||||
_titleLabel.text = model.title;
|
||||
_typeLabel.text = [NSString stringWithFormat:@" %@ ", model.type.uppercaseString];
|
||||
|
||||
if (model.cover.length > 0) {
|
||||
NSURL *imageURL = [NSURL URLWithString:model.cover];
|
||||
[_thumbnailView sd_setImageWithURL:imageURL];
|
||||
} else {
|
||||
_thumbnailView.image = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)prepareForReuse {
|
||||
[super prepareForReuse];
|
||||
[_thumbnailView sd_cancelCurrentImageLoad];
|
||||
_thumbnailView.image = nil;
|
||||
_titleLabel.text = @"";
|
||||
_typeLabel.text = @"";
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// AVVodListViewController.h
|
||||
// SellyCloudSDK_Example
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AVVodListViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
139
Example/SellyCloudSDK/Controllers/VOD/AVVodListViewController.m
Normal file
139
Example/SellyCloudSDK/Controllers/VOD/AVVodListViewController.m
Normal file
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// AVVodListViewController.m
|
||||
// SellyCloudSDK_Example
|
||||
//
|
||||
|
||||
#import "AVVodListViewController.h"
|
||||
#import "AVVodItemModel.h"
|
||||
#import "AVVodItemCell.h"
|
||||
#import "AVLiveStreamModel.h"
|
||||
#import "SCVodVideoPlayerViewController.h"
|
||||
#import "AVConstants.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface AVVodListViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
|
||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||
@property (nonatomic, strong) NSArray<AVVodItemModel *> *vodItems;
|
||||
@end
|
||||
|
||||
@implementation AVVodListViewController
|
||||
|
||||
static NSString * const kVodItemCellIdentifier = @"VodItemCell";
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor systemBackgroundColor];
|
||||
self.title = @"点播测试";
|
||||
|
||||
self.vodItems = @[];
|
||||
[self setupCollectionView];
|
||||
[self fetchVodList];
|
||||
}
|
||||
|
||||
#pragma mark - Network
|
||||
|
||||
- (void)fetchVodList {
|
||||
NSURL *url = [NSURL URLWithString:@"http://rtmp.sellycloud.io:8089/live/sdk/demo/vodlist"];
|
||||
NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
if (error || !data) {
|
||||
NSLog(@"fetchVodList failed: %@", error);
|
||||
return;
|
||||
}
|
||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
if (![json isKindOfClass:NSDictionary.class]) return;
|
||||
|
||||
NSMutableArray<AVVodItemModel *> *items = [NSMutableArray array];
|
||||
[json enumerateKeysAndObjectsUsingBlock:^(NSString *type, NSString *vodUrl, BOOL *stop) {
|
||||
AVVodItemModel *item = [AVVodItemModel modelWithUrl:vodUrl
|
||||
cover:@""
|
||||
title:[NSString stringWithFormat:@"%@ 测试", type.uppercaseString]
|
||||
type:type];
|
||||
[items addObject:item];
|
||||
}];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.vodItems = items.copy;
|
||||
[self.collectionView reloadData];
|
||||
});
|
||||
}];
|
||||
[task resume];
|
||||
}
|
||||
|
||||
#pragma mark - Setup UI
|
||||
|
||||
- (void)setupCollectionView {
|
||||
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
||||
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
|
||||
layout.minimumInteritemSpacing = 12;
|
||||
layout.minimumLineSpacing = 16;
|
||||
layout.sectionInset = UIEdgeInsetsMake(16, 16, 16, 16);
|
||||
|
||||
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;
|
||||
[self.collectionView registerClass:[AVVodItemCell class] forCellWithReuseIdentifier:kVodItemCellIdentifier];
|
||||
|
||||
[self.view addSubview:self.collectionView];
|
||||
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
|
||||
make.left.right.bottom.equalTo(self.view);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDataSource
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.vodItems.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
AVVodItemCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kVodItemCellIdentifier forIndexPath:indexPath];
|
||||
[cell configureWithModel:self.vodItems[indexPath.item]];
|
||||
return cell;
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDelegateFlowLayout
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
CGFloat totalHorizontalPadding = 16 * 2 + 12;
|
||||
CGFloat availableWidth = collectionView.bounds.size.width - totalHorizontalPadding;
|
||||
CGFloat itemWidth = availableWidth / 2.0;
|
||||
CGFloat thumbnailHeight = itemWidth * (9.0 / 16.0);
|
||||
CGFloat itemHeight = thumbnailHeight + 6 + 18 + 6; // thumbnail + spacing + title (1 line) + bottom
|
||||
return CGSizeMake(itemWidth, itemHeight);
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDelegate
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
|
||||
|
||||
AVVodItemModel *item = self.vodItems[indexPath.item];
|
||||
|
||||
// Convert to AVLiveStreamModel for the player
|
||||
AVLiveStreamModel *stream = [[AVLiveStreamModel alloc] init];
|
||||
stream.url = item.url;
|
||||
|
||||
SCVodVideoPlayerViewController *vc = [[SCVodVideoPlayerViewController alloc] initWithLiveStream:stream];
|
||||
vc.hidesBottomBarWhenPushed = YES;
|
||||
vc.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation Support
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
return UIInterfaceOrientationPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user