initial commit

This commit is contained in:
Caleb
2026-03-01 15:59:27 +08:00
commit a9e97d56cb
1426 changed files with 172367 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
//
// AVCallViewController.h
// AVDemo
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AVCallViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

View 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

View File

@@ -0,0 +1,14 @@
//
// AVHomeViewController.h
// AVDemo
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AVHomeViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,393 @@
//
// AVHomeViewController.m
// AVDemo
//
#import "AVHomeViewController.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];
//
[SellyCloudManager setKiwiAppKey:kiwiAppkey name:"123"];
// 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

View 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

View 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

View File

@@ -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

View File

@@ -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