Files
fanyin-ios/TUIKit/TUIRoomKit/Source/View/ViewModel/QualityInfoViewModel.swift
2025-08-12 14:27:12 +08:00

191 lines
6.8 KiB
Swift

//
// QualityInfoViewModel.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/4/19.
// Copyright © 2024 Tencent. All rights reserved.
//
import Foundation
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol QualityViewResponder: AnyObject {
func reloadData()
}
class QualityCellModel: NSObject {
enum CellType {
case upDown
case normal
}
var titleText: String = ""
var type: CellType = .upDown
var uplinkString: String = ""
var downlinkString: String = ""
var normalString: String = ""
}
class QualitySectionModel: NSObject {
var titleText: String = ""
var items: [QualityCellModel] = []
}
class QualityInfoViewModel: NSObject {
static let timeSuffix = "ms"
static let lossSuffix = "%"
static let bitrateSuffix = "kbps"
static let framerateSuffix = "FPS"
var sections: [QualitySectionModel] = []
weak var viewResponder: QualityViewResponder? = nil
private var rttCellModel: QualityCellModel = {
var rttCellModel = QualityCellModel()
rttCellModel.titleText = .rttString
rttCellModel.normalString = "0" + timeSuffix
rttCellModel.type = .normal
return rttCellModel
}()
private var lossCellModel: QualityCellModel = {
var lossCellModel = QualityCellModel()
lossCellModel.titleText = .lossString
lossCellModel.uplinkString = "0" + lossSuffix
lossCellModel.downlinkString = "0" + lossSuffix
return lossCellModel
}()
private var audioBitrateCellModel: QualityCellModel = {
var bitrateCellModel = QualityCellModel()
bitrateCellModel.titleText = .bitrateString
bitrateCellModel.uplinkString = "0" + bitrateSuffix
bitrateCellModel.downlinkString = "0" + bitrateSuffix
return bitrateCellModel
}()
private var videoResCellModel: QualityCellModel = {
var resCellModel = QualityCellModel()
resCellModel.titleText = .resolutionString
resCellModel.uplinkString = "0x0"
resCellModel.downlinkString = "0x0"
return resCellModel
}()
private var videoFrameRateCellModel: QualityCellModel = {
var frameCellModel = QualityCellModel()
frameCellModel.titleText = .frameRateString
frameCellModel.uplinkString = "0" + framerateSuffix
frameCellModel.downlinkString = "0" + framerateSuffix
return frameCellModel
}()
private var videoBitrateCellModel: QualityCellModel = {
var bitrateCellModel = QualityCellModel()
bitrateCellModel.titleText = .bitrateString
bitrateCellModel.uplinkString = "0" + bitrateSuffix
bitrateCellModel.downlinkString = "0" + bitrateSuffix
return bitrateCellModel
}()
override init() {
super.init()
self.addSection(with: [self.rttCellModel, self.lossCellModel], title: .networkString)
self.addSection(with: [self.audioBitrateCellModel], title: .audioString)
self.addSection(with: [self.videoResCellModel, self.videoFrameRateCellModel, self.videoBitrateCellModel], title: .videoString)
subscribeEngine()
}
private func addSection(with items: [QualityCellModel], title: String) {
let section = QualitySectionModel()
section.titleText = title
section.items = items
self.sections.append(section)
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onStatistics, observer: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onStatistics, observer: self)
}
deinit {
unsubscribeEngine()
}
}
extension QualityInfoViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onStatistics:
guard let data = param?["statistics"] as? TRTCStatistics else { return }
handleStatistics(data: data)
default:
break
}
}
private func handleStatistics(data: TRTCStatistics) {
let localStatistics = data.localStatistics.first(where: { $0.streamType == .big })
let remoteStatistics = data.remoteStatistics ?? []
self.rttCellModel.normalString = String(data.rtt) + QualityInfoViewModel.timeSuffix
// assemble uplink data
self.lossCellModel.uplinkString = String(data.upLoss) + QualityInfoViewModel.lossSuffix
self.audioBitrateCellModel.uplinkString = String(localStatistics?.audioBitrate ?? 0) + QualityInfoViewModel.bitrateSuffix
self.videoResCellModel.uplinkString = String(localStatistics?.width ?? 0) + "x" + String(localStatistics?.height ?? 0)
self.videoFrameRateCellModel.uplinkString = String(localStatistics?.frameRate ?? 0) + QualityInfoViewModel.framerateSuffix
self.videoBitrateCellModel.uplinkString = String(localStatistics?.videoBitrate ?? 0) + QualityInfoViewModel.bitrateSuffix
// assemble downlink data
let remoteSumAudioBitrate = remoteStatistics.reduce(0) { sum, stream in
return sum + stream.audioBitrate
}
let remoteMaxFramerate = remoteStatistics.max(by: {$0.frameRate < $1.frameRate})
let remoteMaxVideoRes = remoteStatistics.max(by: { $0.width * $0.height < $1.width * $1.height })
let remoteSumVideoBitrate = remoteStatistics.reduce(0) { sum, stream in
return sum + stream.videoBitrate
}
self.lossCellModel.downlinkString = String(data.downLoss) + QualityInfoViewModel.lossSuffix
self.audioBitrateCellModel.downlinkString = String(remoteSumAudioBitrate) + QualityInfoViewModel.bitrateSuffix
self.videoResCellModel.downlinkString = String(remoteMaxVideoRes?.width ?? 0) + "x" + String(remoteMaxVideoRes?.height ?? 0)
self.videoFrameRateCellModel.downlinkString = String(remoteMaxFramerate?.frameRate ?? 0) + QualityInfoViewModel.framerateSuffix
self.videoBitrateCellModel.downlinkString = String(remoteSumVideoBitrate) + QualityInfoViewModel.bitrateSuffix
self.viewResponder?.reloadData()
}
}
private extension String {
static var networkString: String {
localized("Network")
}
static var audioString: String {
localized("Audio")
}
static var videoString: String {
localized("Video")
}
static var rttString: String {
localized("Latency")
}
static var lossString: String {
localized("Packet Loss Rate")
}
static var bitrateString: String {
localized("Bitrate")
}
static var resolutionString: String {
localized("Resolution")
}
static var frameRateString: String {
localized("Frame Rate")
}
}