Files
SellyCloudSDK_demo/Example/SellyCloudSDK/Play/SCPlayerConfigView.m
2026-04-07 18:20:16 +08:00

442 lines
17 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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