382 lines
16 KiB
Objective-C
382 lines
16 KiB
Objective-C
//
|
||
// 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
|