Files
SellyCloudSDK_demo/Example/SellyCloudSDK/Controllers/Home/AVHomeViewController.m
2026-04-07 18:20:16 +08:00

382 lines
16 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 "AVApiService.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;
@end
@implementation AVHomeViewController
static NSString * const kLiveStreamCellIdentifier = @"LiveStreamCell";
- (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];
[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;
[[AVApiService shared] fetchLiveStreams:^(NSArray<AVLiveStreamModel *> *streams) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf.refreshControl endRefreshing];
strongSelf.liveStreams = streams;
NSLog(@"✅ 成功获取 %ld 个直播流", (long)streams.count);
[strongSelf.collectionView reloadData];
} failure:^(NSError *error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf.refreshControl endRefreshing];
NSLog(@"❌ 网络请求失败: %@", error.localizedDescription);
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
message: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];
liveStream.xorKey = config.xorKey;
// 根据协议类型设置 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