Initial clean commit

This commit is contained in:
2026-03-26 12:05:37 +08:00
commit 7c3c8dffee
1177 changed files with 147248 additions and 0 deletions

View 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 30
//
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) {
// 16612
make.left.equalTo(self.headerView).offset(16);
make.right.equalTo(self.headerView.mas_centerX).offset(-6);
} else if (i == 1) {
// 616
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