// // SCLiveStatsView.m // SellyCloudSDK_Example // // Created by Caleb on 21/10/25. // Copyright © 2025 Caleb. All rights reserved. // #import "SCLiveStatsView.h" @interface SCLiveStatsView () @property (nonatomic, strong) UIVisualEffectView *blurView; @property (nonatomic, strong) UIView *headerView; @property (nonatomic, strong) UIView *contentView; @property (nonatomic, strong) UIStackView *statsStackView; @property (nonatomic, strong) UILabel *titleLabel; // Stats items @property (nonatomic, strong) UIView *protocolItem; @property (nonatomic, strong) UIView *appCpuItem; @property (nonatomic, strong) UIView *sysCpuItem; @property (nonatomic, strong) UIView *bitrateItem; @property (nonatomic, strong) UIView *fpsItem; @property (nonatomic, strong) UIView *rttItem; @property (nonatomic, strong) UIView *packetLossItem; // 新增:丢包率 @property (nonatomic, strong) UILabel *protocolLabel; @property (nonatomic, strong) UILabel *appCpuLabel; @property (nonatomic, strong) UILabel *sysCpuLabel; @property (nonatomic, strong) UILabel *bitrateLabel; @property (nonatomic, strong) UILabel *fpsLabel; @property (nonatomic, strong) UILabel *rttLabel; @property (nonatomic, strong) UILabel *packetLossLabel; // 新增:丢包率标签 @property (nonatomic, strong) UIButton *toggleButton; @property (nonatomic, assign) BOOL isExpanded; @property (nonatomic, strong) MASConstraint *contentViewHeightConstraint; @end @implementation SCLiveStatsView - (instancetype)init { self = [super init]; if (self) { _isExpanded = YES; // 默认展开 [self setupView]; } return self; } - (void)setupView { // 毛玻璃背景 UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemUltraThinMaterialDark]; _blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; _blurView.layer.cornerRadius = 12; _blurView.layer.masksToBounds = YES; [self addSubview:_blurView]; [_blurView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self); }]; // 边框 self.layer.cornerRadius = 12; self.layer.borderWidth = 0.5; self.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3].CGColor; // 标题栏(可点击) _headerView = [[UIView alloc] init]; [_blurView.contentView addSubview:_headerView]; [_headerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.equalTo(_blurView.contentView); make.height.offset(44); }]; // 添加点击手势 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleButtonTapped)]; [_headerView addGestureRecognizer:tap]; // 标题 _titleLabel = [[UILabel alloc] init]; _titleLabel.text = @"📊 直播数据"; _titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]; _titleLabel.textColor = [UIColor whiteColor]; [_headerView addSubview:_titleLabel]; [_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(_headerView); make.left.offset(12); }]; // 展开/收起按钮 _toggleButton = [UIButton buttonWithType:UIButtonTypeSystem]; // 默认展开状态,显示向上箭头(表示可以收起) [_toggleButton setImage:[UIImage systemImageNamed:@"chevron.up.circle.fill"] forState:UIControlStateNormal]; _toggleButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8]; [_toggleButton addTarget:self action:@selector(toggleButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [_headerView addSubview:_toggleButton]; [_toggleButton mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(_headerView); make.right.offset(-12); make.width.height.offset(24); }]; // 分割线 UIView *separatorLine = [[UIView alloc] init]; separatorLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2]; [_blurView.contentView addSubview:separatorLine]; [separatorLine mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(_headerView.mas_bottom); make.left.offset(12); make.right.offset(-12); make.height.offset(0.5); }]; // 内容容器 _contentView = [[UIView alloc] init]; _contentView.clipsToBounds = YES; [_blurView.contentView addSubview:_contentView]; [_contentView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(separatorLine.mas_bottom); make.left.right.bottom.equalTo(_blurView.contentView); // 保存高度约束的引用,用于动态调整 self.contentViewHeightConstraint = make.height.offset(0); }]; // StackView 用于统一管理数据项 _statsStackView = [[UIStackView alloc] init]; _statsStackView.axis = UILayoutConstraintAxisVertical; _statsStackView.spacing = 6; _statsStackView.distribution = UIStackViewDistributionFill; // 改为 Fill,因为我们会手动设置每个 item 的高度 _statsStackView.alignment = UIStackViewAlignmentFill; [_contentView addSubview:_statsStackView]; [_statsStackView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.offset(12); make.left.offset(12); make.right.offset(-12); make.bottom.offset(-12); }]; // 创建数据项 _protocolItem = [self createStatsItemWithIcon:@"network" label:_protocolLabel = [self createValueLabel]]; _appCpuItem = [self createStatsItemWithIcon:@"cpu" label:_appCpuLabel = [self createValueLabel]]; _sysCpuItem = [self createStatsItemWithIcon:@"cpu.fill" label:_sysCpuLabel = [self createValueLabel]]; _bitrateItem = [self createStatsItemWithIcon:@"speedometer" label:_bitrateLabel = [self createValueLabel]]; _fpsItem = [self createStatsItemWithIcon:@"film" label:_fpsLabel = [self createValueLabel]]; _rttItem = [self createStatsItemWithIcon:@"timer" label:_rttLabel = [self createValueLabel]]; _packetLossItem = [self createStatsItemWithIcon:@"exclamationmark.triangle" label:_packetLossLabel = [self createValueLabel]]; // 新增丢包率项 [_statsStackView addArrangedSubview:_protocolItem]; [_statsStackView addArrangedSubview:_appCpuItem]; [_statsStackView addArrangedSubview:_sysCpuItem]; [_statsStackView addArrangedSubview:_bitrateItem]; [_statsStackView addArrangedSubview:_fpsItem]; [_statsStackView addArrangedSubview:_rttItem]; [_statsStackView addArrangedSubview:_packetLossItem]; // 添加到 StackView // 设置每个 item 的高度 for (UIView *item in _statsStackView.arrangedSubviews) { [item mas_makeConstraints:^(MASConstraintMaker *make) { make.height.offset(20); }]; } // 计算展开时的内容高度:item高度 * 数量 + 间距 * (数量-1) + 上下padding CGFloat expandedHeight = 20 * 7 + 6 * 6 + 24; // 20*7 + 6*6 + 12*2 = 200 [self.contentViewHeightConstraint setOffset:expandedHeight]; } - (UIView *)createStatsItemWithIcon:(NSString *)iconName label:(UILabel *)valueLabel { UIView *container = [[UIView alloc] init]; // 图标 UIImageView *iconView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:iconName]]; iconView.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8]; iconView.contentMode = UIViewContentModeScaleAspectFit; [container addSubview:iconView]; [iconView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.offset(0); make.centerY.equalTo(container); make.width.height.offset(14); }]; // 值标签 [container addSubview:valueLabel]; [valueLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(iconView.mas_right).offset(8); make.centerY.equalTo(container); make.right.offset(0); }]; return container; } - (UILabel *)createValueLabel { UILabel *label = [[UILabel alloc] init]; label.font = [UIFont monospacedSystemFontOfSize:11 weight:UIFontWeightMedium]; label.textColor = [UIColor whiteColor]; label.adjustsFontSizeToFitWidth = YES; label.minimumScaleFactor = 0.7; label.numberOfLines = 1; return label; } - (void)toggleButtonTapped { self.isExpanded = !self.isExpanded; // 计算高度 CGFloat expandedHeight = 20 * 7 + 6 * 6 + 24; // item高度 * 数量 + 间距 * (数量-1) + 上下padding CGFloat collapsedHeight = 0; [UIView animateWithDuration:0.3 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0.5 options:UIViewAnimationOptionCurveEaseInOut animations:^{ if (self.isExpanded) { // 展开状态:显示向上箭头(表示可以收起) [self.toggleButton setImage:[UIImage systemImageNamed:@"chevron.up.circle.fill"] forState:UIControlStateNormal]; [self.contentViewHeightConstraint setOffset:expandedHeight]; } else { // 收起状态:显示向下箭头(表示可以展开) [self.toggleButton setImage:[UIImage systemImageNamed:@"chevron.down.circle.fill"] forState:UIControlStateNormal]; [self.contentViewHeightConstraint setOffset:collapsedHeight]; } // 显示/隐藏内容(用 alpha 做淡入淡出效果) self.contentView.alpha = self.isExpanded ? 1.0 : 0.0; // 更新约束触发布局更新 [self.superview layoutIfNeeded]; } completion:nil]; // 触觉反馈 UIImpactFeedbackGenerator *feedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; [feedback impactOccurred]; } - (void)setStats:(SellyLivePusherStats *)stats { _stats = stats; // 协议 + streamId if (self.streamId.length > 0) { self.protocolLabel.text = [NSString stringWithFormat:@"%@,streamId:%@", stats.protocol, self.streamId]; } else { self.protocolLabel.text = stats.protocol; } // App CPU(独立显示) UIColor *appCpuColor = [self colorForCPU:stats.appCpu]; NSMutableAttributedString *appCpuText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"App CPU: %ld%%", stats.appCpu]]; [appCpuText addAttribute:NSForegroundColorAttributeName value:appCpuColor range:NSMakeRange(0, appCpuText.length)]; self.appCpuLabel.attributedText = appCpuText; // System CPU(独立显示) UIColor *sysCpuColor = [self colorForCPU:stats.systemCpu]; NSMutableAttributedString *sysCpuText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Sys CPU: %ld%%", stats.systemCpu]]; [sysCpuText addAttribute:NSForegroundColorAttributeName value:sysCpuColor range:NSMakeRange(0, sysCpuText.length)]; self.sysCpuLabel.attributedText = sysCpuText; // 码率(音频 + 视频) NSInteger totalBitrate = stats.audioBitrate + stats.videoBitrate; self.bitrateLabel.text = [NSString stringWithFormat:@"%ld kbps (A:%ld V:%ld)", totalBitrate, stats.audioBitrate, stats.videoBitrate]; // FPS(根据帧率设置颜色) UIColor *fpsColor = [self colorForFPS:stats.fps]; NSMutableAttributedString *fpsText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"FPS: %ld", stats.fps]]; [fpsText addAttribute:NSForegroundColorAttributeName value:fpsColor range:NSMakeRange(0, fpsText.length)]; self.fpsLabel.attributedText = fpsText; // RTT(根据延迟设置颜色) UIColor *rttColor = [self colorForRTT:stats.rtt]; NSMutableAttributedString *rttText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"RTT: %ld ms", stats.rtt]]; [rttText addAttribute:NSForegroundColorAttributeName value:rttColor range:NSMakeRange(0, rttText.length)]; self.rttLabel.attributedText = rttText; // 丢包率(根据丢包率设置颜色) UIColor *packetLossColor = [self colorForPacketLoss:stats.packetLossRate]; NSMutableAttributedString *packetLossText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"丢包: %.2f%%", stats.packetLossRate]]; [packetLossText addAttribute:NSForegroundColorAttributeName value:packetLossColor range:NSMakeRange(0, packetLossText.length)]; self.packetLossLabel.attributedText = packetLossText; } #pragma mark - Helper Methods - (UIColor *)colorForCPU:(NSInteger)cpu { if (cpu < 40) { return [UIColor colorWithRed:0.3 green:0.85 blue:0.39 alpha:1.0]; // 绿色 } else if (cpu < 70) { return [UIColor colorWithRed:1.0 green:0.8 blue:0.0 alpha:1.0]; // 黄色 } else { return [UIColor colorWithRed:1.0 green:0.23 blue:0.19 alpha:1.0]; // 红色 } } - (UIColor *)colorForFPS:(NSInteger)fps { if (fps >= 25) { return [UIColor colorWithRed:0.3 green:0.85 blue:0.39 alpha:1.0]; // 绿色 } else if (fps >= 15) { return [UIColor colorWithRed:1.0 green:0.8 blue:0.0 alpha:1.0]; // 黄色 } else { return [UIColor colorWithRed:1.0 green:0.23 blue:0.19 alpha:1.0]; // 红色 } } - (UIColor *)colorForRTT:(NSInteger)rtt { if (rtt < 100) { return [UIColor colorWithRed:0.3 green:0.85 blue:0.39 alpha:1.0]; // 绿色 } else if (rtt < 300) { return [UIColor colorWithRed:1.0 green:0.8 blue:0.0 alpha:1.0]; // 黄色 } else { return [UIColor colorWithRed:1.0 green:0.23 blue:0.19 alpha:1.0]; // 红色 } } - (UIColor *)colorForPacketLoss:(CGFloat)packetLoss { if (packetLoss < 2.0) { return [UIColor colorWithRed:0.3 green:0.85 blue:0.39 alpha:1.0]; // 绿色:优秀 } else if (packetLoss < 5.0) { return [UIColor colorWithRed:1.0 green:0.8 blue:0.0 alpha:1.0]; // 黄色:一般 } else { return [UIColor colorWithRed:1.0 green:0.23 blue:0.19 alpha:1.0]; // 红色:差 } } @end