[feat] add mobile codec status and scrolling codec names

This commit is contained in:
dijunkun
2026-10-27 04:16:41 +08:00
parent 1687816bde
commit e123d5f2cc
20 changed files with 241 additions and 48 deletions
@@ -136,7 +136,10 @@ typedef struct {
didReceiveFileAtURL:(NSURL *)fileURL;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didUpdateNetworkStats:(CrossDeskNetworkStats)stats
NS_SWIFT_NAME(rtcBridge(_:didUpdateNetworkStats:));
localDecoder:(NSString *)decoder
NS_SWIFT_NAME(rtcBridge(_:didUpdateNetworkStats:localDecoder:));
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didUpdateRemoteVideoEncoder:(NSString *)encoder;
@end
/// Objective-C++ boundary around the native MiniRTC C API.
@@ -1434,6 +1434,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
action.type = ControlType::host_infomation;
action.i.platform = crossdesk::HostPlatform::IOS;
action.i.supports_privacy_screen = true;
action.i.supports_video_codec_status = true;
CopyCString(action.i.host_name, sizeof(action.i.host_name),
controller_name);
action.i.host_name_size = std::strlen(action.i.host_name);
@@ -1719,6 +1720,16 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
if (source == kControlStream) {
RemoteAction action{};
if (!action.from_json(std::string(data, size))) return;
if (action.type == ControlType::video_codec_status) {
NSString *encoder = [NSString stringWithUTF8String:action.vcs.encoder];
DispatchMain(^{
if (![self isControllerGenerationActive:generation]) return;
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(rtcBridge:didUpdateRemoteVideoEncoder:)])
[delegate rtcBridge:self didUpdateRemoteVideoEncoder:encoder ?: @""];
});
return;
}
if (action.type == ControlType::privacy_status) {
const auto message = action.to_json();
NSData *status = [NSData dataWithBytes:message.data() length:message.size()];
@@ -2034,13 +2045,17 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
mode == TraversalMode::P2P ? CrossDeskTraversalModeDirect
: mode == TraversalMode::Relay ? CrossDeskTraversalModeRelay
: CrossDeskTraversalModeUnknown};
NSString *decoder = [[NSString alloc]
initWithBytes:stats->video_decoder
length:strnlen(stats->video_decoder, sizeof(stats->video_decoder))
encoding:NSUTF8StringEncoding];
DispatchMain(^{
// Disconnect/reconnect can happen while this report waits on the UI queue.
if (![self isControllerGenerationActive:generation]) return;
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:
@selector(rtcBridge:didUpdateNetworkStats:)]) {
[delegate rtcBridge:self didUpdateNetworkStats:snapshot];
@selector(rtcBridge:didUpdateNetworkStats:localDecoder:)]) {
[delegate rtcBridge:self didUpdateNetworkStats:snapshot localDecoder:decoder ?: @""];
}
});
}
@@ -16,6 +16,7 @@ struct RemoteNetworkReport {
var mode: Mode = .unknown
var srtpActive = false
var rttMilliseconds: Double = -1
var localVideoDecoder = ""
}
struct RemoteNetworkSnapshot {
@@ -23,6 +24,8 @@ struct RemoteNetworkSnapshot {
var framesPerSecond = 0
var videoLatencyMilliseconds: Double?
var rttMilliseconds: Double?
var remoteVideoEncoder = ""
var localVideoDecoder = ""
var connectionMode: String {
switch report?.mode {
@@ -66,6 +69,11 @@ struct RemoteNetworkStatistics {
private var frames: [(time: TimeInterval, latency: Double?)] = []
private var displayedSnapshot = RemoteNetworkSnapshot()
private var displayTime: TimeInterval?
private var remoteVideoEncoder = ""
mutating func receiveRemoteVideoEncoder(_ encoder: String) {
remoteVideoEncoder = encoder
}
mutating func receive(_ report: RemoteNetworkReport, at now: TimeInterval) {
self.report = report
@@ -117,7 +125,9 @@ struct RemoteNetworkStatistics {
report: now - reportTime < 3 ? report : nil,
framesPerSecond: recentFrames.count,
videoLatencyMilliseconds: delays.isEmpty ? nil : delays.reduce(0, +) / Double(delays.count),
rttMilliseconds: now - rttTime < 3 ? averageRTT : nil
rttMilliseconds: now - rttTime < 3 ? averageRTT : nil,
remoteVideoEncoder: remoteVideoEncoder,
localVideoDecoder: report?.localVideoDecoder ?? ""
)
}
}
@@ -1473,7 +1473,14 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
func rtcBridge(_ bridge: CrossDeskRTCBridge,
didUpdateNetworkStats stats: CrossDeskNetworkStats) {
didUpdateRemoteVideoEncoder encoder: String) {
guard hasNetworkConsent, isConnected else { return }
networkStatistics.receiveRemoteVideoEncoder(encoder)
}
func rtcBridge(_ bridge: CrossDeskRTCBridge,
didUpdateNetworkStats stats: CrossDeskNetworkStats,
localDecoder: String) {
guard hasNetworkConsent, isConnected else { return }
func traffic(_ value: CrossDeskTrafficStats) -> RemoteTrafficStatistics {
RemoteTrafficStatistics(inboundBitrate: value.inboundBitrate,
@@ -1489,7 +1496,8 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
networkStatistics.receive(RemoteNetworkReport(
video: traffic(stats.video), audio: traffic(stats.audio),
data: traffic(stats.data), total: traffic(stats.total), mode: mode,
srtpActive: stats.srtpActive.boolValue, rttMilliseconds: stats.rttMilliseconds
srtpActive: stats.srtpActive.boolValue, rttMilliseconds: stats.rttMilliseconds,
localVideoDecoder: localDecoder
), at: ProcessInfo.processInfo.systemUptime)
}
@@ -1327,6 +1327,47 @@ private struct FloatingSessionMenu: View {
}
}
private struct ScrollingCodecName: View {
let value: String
@State private var textWidth: CGFloat = 0
@State private var startedAt = Date()
@ScaledMetric(relativeTo: .caption) private var lineHeight = 16.0
var body: some View {
GeometryReader { bounds in
TimelineView(.animation(minimumInterval: 1.0 / 30, paused: textWidth <= bounds.size.width)) { context in
Text(value)
.lineLimit(1)
.fixedSize()
.background {
GeometryReader { text in
Color.clear
.onAppear { textWidth = text.size.width }
.onChange(of: text.size.width) { textWidth = $0; startedAt = Date() }
}
}
.offset(x: offset(in: bounds.size.width, at: context.date))
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .leading)
.clipped()
}
.onAppear { startedAt = Date() }
.onChange(of: value) { _ in startedAt = Date() }
.onChange(of: bounds.size.width) { _ in startedAt = Date() }
}
.frame(height: lineHeight)
.accessibilityElement(children: .ignore)
.accessibilityLabel(value)
}
private func offset(in width: CGFloat, at time: Date) -> CGFloat {
let distance = max(0, textWidth - width)
guard distance > 0 else { return width - textWidth }
// Pause for one second at each end, then repeat at 30 points/second.
let phase = max(0, time.timeIntervalSince(startedAt)).truncatingRemainder(dividingBy: Double(distance) / 30 + 2)
return -min(distance, CGFloat(max(0, phase - 1) * 30))
}
}
private struct FloatingNetworkStatistics: View {
let snapshot: RemoteNetworkSnapshot
let frameSize: CGSize
@@ -1366,6 +1407,8 @@ private struct FloatingNetworkStatistics: View {
}
}
.accessibilityElement(children: .combine)
codec("被控端编码器", value: snapshot.remoteVideoEncoder)
codec("控制端解码器", value: snapshot.localVideoDecoder)
}
.font(.caption.monospacedDigit())
}
@@ -1386,6 +1429,14 @@ private struct FloatingNetworkStatistics: View {
.accessibilityElement(children: .combine)
}
private func codec(_ title: String, value: String) -> some View {
HStack(spacing: 8) {
Text(title).foregroundStyle(.secondary).fixedSize()
ScrollingCodecName(value: value.isEmpty ? "—" : value)
}
.accessibilityElement(children: .combine)
}
private func trafficRow(_ title: String, statistics: RemoteTrafficStatistics?) -> some View {
trafficRow(title,
inbound: RemoteNetworkSnapshot.bitrate(statistics?.inboundBitrate),