324 lines
14 KiB
Objective-C
324 lines
14 KiB
Objective-C
//
|
||
// 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
|