feat: 支持手动切换采集帧率

在 Android、Web 及 Flutter 控制端新增帧率选择 UI,被控端按屏幕刷新率生成支持的帧率档位并上报,允许在保持分辨率不变的情况下仅切换帧率。

另附带更新信号服务器的数据库配置。
This commit is contained in:
TongTongStudio
2026-07-31 03:26:40 +08:00
parent c3602493dd
commit 362b9f238a
23 changed files with 483 additions and 13 deletions

View File

@@ -34,6 +34,8 @@ struct ControlMessage {
var height: Int32 = 0 // field 13, varint
var fps: Int32 = 0 // field 14, varint
var streamMode: Int32 = 0 // field 15, varint (0=WebRTC 1=)
/// REPORT_RESOLUTION
var supportedFps: [Int32] = [] // field 16, repeated varint (packed)
// MARK: - Encode
@@ -54,6 +56,16 @@ struct ControlMessage {
w.writeVarintField(13, UInt64(bitPattern: Int64(height)))
w.writeVarintField(14, UInt64(bitPattern: Int64(fps)))
w.writeVarintField(15, UInt64(bitPattern: Int64(streamMode)))
// repeated int32 supported_fps = 16packed
if !supportedFps.isEmpty {
var payload = ProtoWriter()
for v in supportedFps {
payload.writeVarint(UInt64(bitPattern: Int64(v)))
}
w.writeVarint(UInt64(16 << 3 | 2))
w.writeVarint(UInt64(payload.data.count))
w.data.append(payload.data)
}
return w.data
}
@@ -81,6 +93,19 @@ struct ControlMessage {
case (13, 0): guard let v = r.readVarint() else { return nil }; msg.height = Int32(truncatingIfNeeded: Int64(bitPattern: v))
case (14, 0): guard let v = r.readVarint() else { return nil }; msg.fps = Int32(truncatingIfNeeded: Int64(bitPattern: v))
case (15, 0): guard let v = r.readVarint() else { return nil }; msg.streamMode = Int32(truncatingIfNeeded: Int64(bitPattern: v))
case (16, 2):
// packed repeated int32 varint
guard let len = r.readVarint() else { return nil }
let end = r.offset + Int(len)
guard end <= data.endIndex else { return nil }
while r.offset < end {
guard let v = r.readVarint() else { return nil }
msg.supportedFps.append(Int32(truncatingIfNeeded: Int64(bitPattern: v)))
}
case (16, 0):
// packed varint
guard let v = r.readVarint() else { return nil }
msg.supportedFps.append(Int32(truncatingIfNeeded: Int64(bitPattern: v)))
default:
// wire type
if !r.skip(wireType: wire) { return nil }

View File

@@ -56,6 +56,10 @@ final class ControllerViewModel: NSObject, ObservableObject {
@Published var statsText: String = ""
@Published var streamMode: StreamMode = .webrtc
@Published var selectedResolution: ResolutionOption = ResolutionOption.all[0]
/// supported_fps
@Published var fpsOptions: [Int] = [15, 24, 30, 60]
/// 0
@Published var currentFps: Int = 0
/// /
@Published var videoAspect: CGFloat = 9.0 / 16.0
@Published var showAuthSheet: Bool = false
@@ -82,6 +86,9 @@ final class ControllerViewModel: NSObject, ObservableObject {
private var pendingAuthValue: String = ""
///
private var suppressStreamModeSend = false
/// 0
private var lastReportedWidth: Int32 = 0
private var lastReportedHeight: Int32 = 0
override init() {
super.init()
@@ -138,6 +145,10 @@ final class ControllerViewModel: NSObject, ObservableObject {
statsText = ""
streamMode = .webrtc
selectedResolution = ResolutionOption.all[0]
fpsOptions = [15, 24, 30, 60]
currentFps = 0
lastReportedWidth = 0
lastReportedHeight = 0
videoAspect = 9.0 / 16.0
}
@@ -192,6 +203,14 @@ final class ControllerViewModel: NSObject, ObservableObject {
webRTCClient?.requestResolutionChange(width: option.width, height: option.height, fps: 0)
}
/// 沿0
func selectFps(_ fps: Int) {
guard fps > 0, fps != currentFps else { return }
webRTCClient?.requestResolutionChange(width: lastReportedWidth,
height: lastReportedHeight,
fps: Int32(fps))
}
func toggleStreamMode(_ selfCodecOn: Bool) {
let newMode: StreamMode = selfCodecOn ? .selfCodec : .webrtc
guard newMode != streamMode else { return }
@@ -441,10 +460,17 @@ extension ControllerViewModel: WebRTCClientDelegate {
func webRTCClient(didReportResolution width: Int, height: Int) {
guard width > 0, height > 0 else { return }
lastReportedWidth = Int32(width)
lastReportedHeight = Int32(height)
if streamMode == .webrtc {
videoAspect = CGFloat(width) / CGFloat(height)
}
}
func webRTCClient(didReportFps fps: Int, supportedFps: [Int]) {
if fps > 0 { currentFps = fps }
if !supportedFps.isEmpty { fpsOptions = supportedFps }
}
}
// MARK: - RTCVideoViewDelegateWebRTC

View File

@@ -147,6 +147,25 @@ private struct ControlPanelView: View {
.font(.footnote)
}
// REPORT_RESOLUTION
Menu {
ForEach(viewModel.fpsOptions, id: \.self) { fps in
Button {
viewModel.selectFps(fps)
} label: {
if fps == viewModel.currentFps {
Label("\(fps)fps", systemImage: "checkmark")
} else {
Text("\(fps)fps")
}
}
}
} label: {
Label(viewModel.currentFps > 0 ? "\(viewModel.currentFps)fps" : "帧率",
systemImage: "speedometer")
.font(.footnote)
}
Button(role: .destructive) {
viewModel.disconnect()
} label: {

View File

@@ -12,6 +12,8 @@ protocol WebRTCClientDelegate: AnyObject {
func webRTCClient(didReportStreamMode mode: Int)
///
func webRTCClient(didReportResolution width: Int, height: Int)
///
func webRTCClient(didReportFps fps: Int, supportedFps: [Int])
}
/// WebRTC PeerConnection Offer/Answer
@@ -313,6 +315,8 @@ extension WebRTCClient: RTCDataChannelDelegate {
self.delegate?.webRTCClient(didReportStreamMode: Int(msg.streamMode))
case .reportResolution:
self.delegate?.webRTCClient(didReportResolution: Int(msg.width), height: Int(msg.height))
self.delegate?.webRTCClient(didReportFps: Int(msg.fps),
supportedFps: msg.supportedFps.map(Int.init))
default:
break
}