// // SCPlayerConfigView.m // SellyCloudSDK_Example // // Created by Caleb on 16/12/25. // Copyright © 2025 Caleb. All rights reserved. // #import "SCPlayerConfigView.h" #import // 配置保存的 key static NSString * const kSCPlayerConfigProtocol = @"SCPlayerConfigProtocol"; static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId"; @implementation SCPlayerConfig - (void)saveToUserDefaults { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setInteger:self.protocol forKey:kSCPlayerConfigProtocol]; if (self.streamId) { [defaults setObject:self.streamId forKey:kSCPlayerConfigStreamId]; } [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"; return config; } @end @interface SCPlayerConfigView () @property (nonatomic, strong) UIView *contentView; @property (nonatomic, strong) UIVisualEffectView *backgroundView; @property (nonatomic, strong) UISegmentedControl *protocolSegment; @property (nonatomic, strong) UITextField *streamIdField; @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); }]; // 播放按钮 _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(_streamIdField.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; } SCPlayerConfig *config = [[SCPlayerConfig alloc] init]; config.protocol = _protocolSegment.selectedSegmentIndex == 0 ? SellyLiveMode_RTMP : SellyLiveMode_RTC; config.streamId = streamId; // 保存配置 [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 *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; } } #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]; // 计算输入框在屏幕上的位置 CGRect textFieldFrame = [_streamIdField convertRect:_streamIdField.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