442 lines
17 KiB
Objective-C
442 lines
17 KiB
Objective-C
//
|
||
// SCPlayerConfigView.m
|
||
// SellyCloudSDK_Example
|
||
//
|
||
// Created by Caleb on 16/12/25.
|
||
// Copyright © 2025 Caleb. All rights reserved.
|
||
//
|
||
|
||
#import "SCPlayerConfigView.h"
|
||
#import <Masonry/Masonry.h>
|
||
|
||
// 配置保存的 key
|
||
static NSString * const kSCPlayerConfigProtocol = @"SCPlayerConfigProtocol";
|
||
static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
||
static NSString * const kSCPlayerConfigXorKey = @"SCPlayerConfigXorKey";
|
||
|
||
@implementation SCPlayerConfig
|
||
|
||
- (void)saveToUserDefaults {
|
||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||
[defaults setInteger:self.protocol forKey:kSCPlayerConfigProtocol];
|
||
if (self.streamId) {
|
||
[defaults setObject:self.streamId forKey:kSCPlayerConfigStreamId];
|
||
}
|
||
if (self.xorKey.length > 0) {
|
||
[defaults setObject:self.xorKey forKey:kSCPlayerConfigXorKey];
|
||
} else {
|
||
[defaults removeObjectForKey:kSCPlayerConfigXorKey];
|
||
}
|
||
[defaults synchronize];
|
||
}
|
||
|
||
+ (instancetype)loadFromUserDefaults {
|
||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||
|
||
SCPlayerConfig *config = [[SCPlayerConfig alloc] init];
|
||
|
||
// 加载协议,默认为 RTMP
|
||
if ([defaults objectForKey:kSCPlayerConfigProtocol]) {
|
||
config.protocol = [defaults integerForKey:kSCPlayerConfigProtocol];
|
||
} else {
|
||
config.protocol = SellyLiveMode_RTMP;
|
||
}
|
||
|
||
// 加载 streamId
|
||
NSString *streamId = [defaults objectForKey:kSCPlayerConfigStreamId];
|
||
config.streamId = streamId ? streamId : @"test";
|
||
|
||
// 加载 xorKey
|
||
config.xorKey = [defaults objectForKey:kSCPlayerConfigXorKey];
|
||
|
||
return config;
|
||
}
|
||
|
||
@end
|
||
|
||
@interface SCPlayerConfigView () <UITextFieldDelegate>
|
||
@property (nonatomic, strong) UIView *contentView;
|
||
@property (nonatomic, strong) UIVisualEffectView *backgroundView;
|
||
@property (nonatomic, strong) UISegmentedControl *protocolSegment;
|
||
@property (nonatomic, strong) UITextField *streamIdField;
|
||
@property (nonatomic, strong) UITextField *xorKeyField;
|
||
@property (nonatomic, strong) UIButton *playButton;
|
||
@property (nonatomic, copy) void(^callback)(SCPlayerConfig *config);
|
||
@property (nonatomic, strong) MASConstraint *backgroundViewCenterYConstraint; // 保存约束引用
|
||
@end
|
||
|
||
@implementation SCPlayerConfigView
|
||
|
||
- (instancetype)init {
|
||
self = [super init];
|
||
if (self) {
|
||
[self setupView];
|
||
[self loadSavedConfig];
|
||
[self registerKeyboardNotifications];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)dealloc {
|
||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||
}
|
||
|
||
- (void)setupView {
|
||
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
||
|
||
// 添加点击手势 - 点击背景关闭
|
||
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(backgroundTapped:)];
|
||
tapGesture.cancelsTouchesInView = NO; // 不阻止子视图的点击事件
|
||
[self addGestureRecognizer:tapGesture];
|
||
|
||
// 背景模糊效果
|
||
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial];
|
||
_backgroundView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
||
_backgroundView.layer.cornerRadius = 16;
|
||
_backgroundView.layer.masksToBounds = YES;
|
||
_backgroundView.userInteractionEnabled = YES; // 确保内容区域可以响应事件
|
||
[self addSubview:_backgroundView];
|
||
|
||
[_backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
// 不完全居中,稍微向下偏移,避开顶部的关闭按钮
|
||
make.centerX.equalTo(self);
|
||
// 保存 centerY 约束的引用,以便在键盘出现时调整
|
||
self.backgroundViewCenterYConstraint = make.centerY.equalTo(self).offset(30); // 向下偏移30pt
|
||
make.left.offset(40);
|
||
make.right.offset(-40);
|
||
}];
|
||
|
||
// 内容容器
|
||
_contentView = [[UIView alloc] init];
|
||
[_backgroundView.contentView addSubview:_contentView];
|
||
[_contentView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(_backgroundView.contentView).insets(UIEdgeInsetsMake(24, 24, 24, 24));
|
||
}];
|
||
|
||
// 标题
|
||
UILabel *titleLabel = [[UILabel alloc] init];
|
||
titleLabel.text = @"播放配置";
|
||
titleLabel.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold];
|
||
titleLabel.textAlignment = NSTextAlignmentCenter;
|
||
[_contentView addSubview:titleLabel];
|
||
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(_contentView);
|
||
make.left.right.equalTo(_contentView);
|
||
make.height.offset(30);
|
||
}];
|
||
|
||
// 协议选择标签
|
||
UILabel *protocolLabel = [[UILabel alloc] init];
|
||
protocolLabel.text = @"播放协议";
|
||
protocolLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||
[_contentView addSubview:protocolLabel];
|
||
[protocolLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(titleLabel.mas_bottom).offset(24);
|
||
make.left.equalTo(_contentView);
|
||
make.height.offset(22);
|
||
}];
|
||
|
||
// 协议选择器
|
||
_protocolSegment = [[UISegmentedControl alloc] initWithItems:@[@"RTMP", @"RTC"]];
|
||
_protocolSegment.selectedSegmentIndex = 0;
|
||
[_contentView addSubview:_protocolSegment];
|
||
[_protocolSegment mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(protocolLabel.mas_bottom).offset(8);
|
||
make.left.right.equalTo(_contentView);
|
||
make.height.offset(36);
|
||
}];
|
||
|
||
// Stream ID 标签
|
||
UILabel *streamIdLabel = [[UILabel alloc] init];
|
||
streamIdLabel.text = @"Stream ID / URL。请输入 Stream ID 或完整 URL";
|
||
streamIdLabel.font = [UIFont systemFontOfSize:14];
|
||
streamIdLabel.numberOfLines = 0;
|
||
[_contentView addSubview:streamIdLabel];
|
||
[streamIdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(_protocolSegment.mas_bottom).offset(24);
|
||
make.left.equalTo(_contentView);
|
||
make.right.equalTo(_contentView);
|
||
make.height.offset(34);
|
||
}];
|
||
|
||
// Stream ID 输入框
|
||
_streamIdField = [[UITextField alloc] init];
|
||
_streamIdField.placeholder = @"请输入 Stream ID 或完整 URL";
|
||
_streamIdField.borderStyle = UITextBorderStyleRoundedRect;
|
||
_streamIdField.font = [UIFont systemFontOfSize:15];
|
||
_streamIdField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
||
_streamIdField.returnKeyType = UIReturnKeyDone;
|
||
_streamIdField.delegate = self;
|
||
|
||
// 禁用首字母自动大写
|
||
_streamIdField.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||
// 禁用自动更正
|
||
_streamIdField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||
// 设置键盘类型为 URL 类型(更适合输入 URL)
|
||
_streamIdField.keyboardType = UIKeyboardTypeURL;
|
||
|
||
[_contentView addSubview:_streamIdField];
|
||
[_streamIdField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(streamIdLabel.mas_bottom).offset(8);
|
||
make.left.right.equalTo(_contentView);
|
||
make.height.offset(44);
|
||
}];
|
||
|
||
// 加密密钥标签
|
||
UILabel *xorKeyLabel = [[UILabel alloc] init];
|
||
xorKeyLabel.text = @"加密密钥 (Hex),留空不解密";
|
||
xorKeyLabel.font = [UIFont systemFontOfSize:14];
|
||
xorKeyLabel.numberOfLines = 0;
|
||
[_contentView addSubview:xorKeyLabel];
|
||
[xorKeyLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(_streamIdField.mas_bottom).offset(24);
|
||
make.left.right.equalTo(_contentView);
|
||
make.height.offset(20);
|
||
}];
|
||
|
||
// 加密密钥输入框
|
||
_xorKeyField = [[UITextField alloc] init];
|
||
_xorKeyField.placeholder = @"如 AABBCCDD";
|
||
_xorKeyField.borderStyle = UITextBorderStyleRoundedRect;
|
||
_xorKeyField.font = [UIFont systemFontOfSize:15];
|
||
_xorKeyField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
||
_xorKeyField.returnKeyType = UIReturnKeyDone;
|
||
_xorKeyField.delegate = self;
|
||
_xorKeyField.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||
_xorKeyField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||
[_contentView addSubview:_xorKeyField];
|
||
[_xorKeyField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(xorKeyLabel.mas_bottom).offset(8);
|
||
make.left.right.equalTo(_contentView);
|
||
make.height.offset(44);
|
||
}];
|
||
|
||
// 播放按钮
|
||
_playButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
[_playButton setTitle:@"开始播放" forState:UIControlStateNormal];
|
||
_playButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightBold];
|
||
_playButton.backgroundColor = [UIColor systemBlueColor];
|
||
[_playButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||
_playButton.layer.cornerRadius = 12;
|
||
_playButton.layer.masksToBounds = YES;
|
||
[_playButton addTarget:self action:@selector(playButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||
[_contentView addSubview:_playButton];
|
||
[_playButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(_xorKeyField.mas_bottom).offset(32);
|
||
make.left.right.equalTo(_contentView);
|
||
make.height.offset(50);
|
||
make.bottom.equalTo(_contentView);
|
||
}];
|
||
|
||
// 移除点击背景关闭的手势,只能通过播放按钮关闭
|
||
}
|
||
|
||
- (void)playButtonTapped {
|
||
NSString *streamId = [_streamIdField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||
|
||
if (streamId.length == 0) {
|
||
// 显示提示
|
||
[self showAlertWithMessage:@"请输入 Stream ID 或 URL"];
|
||
return;
|
||
}
|
||
|
||
NSString *xorKey = [_xorKeyField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||
if (xorKey.length > 0) {
|
||
if (xorKey.length % 2 != 0) {
|
||
[self showAlertWithMessage:@"加密密钥必须为偶数长度"];
|
||
return;
|
||
}
|
||
NSCharacterSet *hexChars = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"];
|
||
if ([xorKey rangeOfCharacterFromSet:hexChars.invertedSet].location != NSNotFound) {
|
||
[self showAlertWithMessage:@"加密密钥只能包含十六进制字符 (0-9, a-f, A-F)"];
|
||
return;
|
||
}
|
||
}
|
||
|
||
SCPlayerConfig *config = [[SCPlayerConfig alloc] init];
|
||
config.protocol = _protocolSegment.selectedSegmentIndex == 0 ? SellyLiveMode_RTMP : SellyLiveMode_RTC;
|
||
config.streamId = streamId;
|
||
config.xorKey = xorKey.length > 0 ? xorKey : nil;
|
||
|
||
// 保存配置
|
||
[config saveToUserDefaults];
|
||
|
||
if (self.callback) {
|
||
self.callback(config);
|
||
}
|
||
|
||
[self dismiss];
|
||
}
|
||
|
||
- (void)showAlertWithMessage:(NSString *)message {
|
||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
|
||
message:message
|
||
preferredStyle:UIAlertControllerStyleAlert];
|
||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||
|
||
// 获取当前的 viewController
|
||
UIViewController *topVC = [self topViewController];
|
||
if (topVC) {
|
||
[topVC presentViewController:alert animated:YES completion:nil];
|
||
}
|
||
}
|
||
|
||
- (UIViewController *)topViewController {
|
||
UIViewController *topVC = nil;
|
||
UIWindow *keyWindow = nil;
|
||
|
||
// iOS 13+ 获取 keyWindow
|
||
if (@available(iOS 13.0, *)) {
|
||
NSSet<UIScene *> *scenes = [UIApplication sharedApplication].connectedScenes;
|
||
for (UIScene *scene in scenes) {
|
||
if ([scene isKindOfClass:[UIWindowScene class]]) {
|
||
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
||
for (UIWindow *window in windowScene.windows) {
|
||
if (window.isKeyWindow) {
|
||
keyWindow = window;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (keyWindow) break;
|
||
}
|
||
} else {
|
||
keyWindow = [UIApplication sharedApplication].keyWindow;
|
||
}
|
||
|
||
topVC = keyWindow.rootViewController;
|
||
while (topVC.presentedViewController) {
|
||
topVC = topVC.presentedViewController;
|
||
}
|
||
return topVC;
|
||
}
|
||
|
||
- (void)showInViewController:(UIViewController *)viewController callback:(void (^)(SCPlayerConfig * _Nonnull))callback {
|
||
self.callback = callback;
|
||
|
||
// 直接添加到 viewController 的 view 上,而不是 window
|
||
self.frame = viewController.view.bounds;
|
||
[viewController.view addSubview:self];
|
||
|
||
// 动画显示
|
||
self.alpha = 0;
|
||
_backgroundView.transform = CGAffineTransformMakeScale(0.8, 0.8);
|
||
|
||
[UIView animateWithDuration:0.3 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0.5 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||
self.alpha = 1;
|
||
self.backgroundView.transform = CGAffineTransformIdentity;
|
||
} completion:nil];
|
||
}
|
||
|
||
- (void)dismiss {
|
||
[UIView animateWithDuration:0.2 animations:^{
|
||
self.alpha = 0;
|
||
self.backgroundView.transform = CGAffineTransformMakeScale(0.8, 0.8);
|
||
} completion:^(BOOL finished) {
|
||
[self removeFromSuperview];
|
||
}];
|
||
}
|
||
|
||
// 背景点击处理
|
||
- (void)backgroundTapped:(UITapGestureRecognizer *)gesture {
|
||
CGPoint location = [gesture locationInView:self];
|
||
|
||
// 判断点击位置是否在内容区域内
|
||
if (!CGRectContainsPoint(_backgroundView.frame, location)) {
|
||
// 点击了背景区域,关闭弹窗
|
||
NSLog(@"📱 点击背景关闭配置弹窗");
|
||
[self dismiss];
|
||
}
|
||
}
|
||
|
||
- (void)loadSavedConfig {
|
||
SCPlayerConfig *savedConfig = [SCPlayerConfig loadFromUserDefaults];
|
||
|
||
// 设置协议选择器
|
||
_protocolSegment.selectedSegmentIndex = (savedConfig.protocol == SellyLiveMode_RTMP) ? 0 : 1;
|
||
|
||
// 设置 streamId
|
||
if (savedConfig.streamId && savedConfig.streamId.length > 0) {
|
||
_streamIdField.text = savedConfig.streamId;
|
||
}
|
||
|
||
// 设置 xorKey
|
||
_xorKeyField.text = savedConfig.xorKey ?: @"";
|
||
}
|
||
|
||
#pragma mark - UITextFieldDelegate
|
||
|
||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||
[textField resignFirstResponder];
|
||
return YES;
|
||
}
|
||
|
||
#pragma mark - Keyboard Notifications
|
||
|
||
- (void)registerKeyboardNotifications {
|
||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||
selector:@selector(keyboardWillShow:)
|
||
name:UIKeyboardWillShowNotification
|
||
object:nil];
|
||
|
||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||
selector:@selector(keyboardWillHide:)
|
||
name:UIKeyboardWillHideNotification
|
||
object:nil];
|
||
}
|
||
|
||
- (void)keyboardWillShow:(NSNotification *)notification {
|
||
NSDictionary *userInfo = notification.userInfo;
|
||
|
||
// 获取键盘高度
|
||
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||
CGFloat keyboardHeight = keyboardFrame.size.height;
|
||
|
||
// 获取动画时长和曲线
|
||
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
||
|
||
// 计算当前激活输入框在屏幕上的位置
|
||
UITextField *activeField = _xorKeyField.isFirstResponder ? _xorKeyField : _streamIdField;
|
||
CGRect textFieldFrame = [activeField convertRect:activeField.bounds toView:self];
|
||
CGFloat textFieldBottom = CGRectGetMaxY(textFieldFrame);
|
||
|
||
// 计算需要的偏移量
|
||
CGFloat visibleHeight = self.bounds.size.height - keyboardHeight;
|
||
|
||
// 给输入框下方留出一些空间(比如 20pt)
|
||
CGFloat desiredSpace = 20;
|
||
CGFloat offset = 0;
|
||
|
||
if (textFieldBottom + desiredSpace > visibleHeight) {
|
||
// 输入框被键盘遮挡了,需要向上移动
|
||
offset = -(textFieldBottom + desiredSpace - visibleHeight);
|
||
}
|
||
|
||
// 更新约束
|
||
[self.backgroundViewCenterYConstraint setOffset:offset];
|
||
|
||
// 动画更新布局
|
||
[UIView animateWithDuration:duration delay:0 options:(curve << 16) animations:^{
|
||
[self layoutIfNeeded];
|
||
} completion:nil];
|
||
}
|
||
|
||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||
NSDictionary *userInfo = notification.userInfo;
|
||
|
||
// 获取动画时长和曲线
|
||
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
||
|
||
// 恢复原始位置
|
||
[self.backgroundViewCenterYConstraint setOffset:30];
|
||
|
||
// 动画更新布局
|
||
[UIView animateWithDuration:duration delay:0 options:(curve << 16) animations:^{
|
||
[self layoutIfNeeded];
|
||
} completion:nil];
|
||
}
|
||
|
||
@end
|