// // AVHomeViewController.m // AVDemo // #import "AVHomeViewController.h" #import #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 #import #import @interface AVHomeViewController () @property (nonatomic, strong) UIView *headerView; @property (nonatomic, strong) NSArray *headerButtons; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) NSArray *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 *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