Aggiunge bitrate adattivo con qualità minima da regia e telemetria del telefono.

L'encoder scende in congestione e risale piano; la regia imposta il pavimento, mostra lo stato del telefono e apre il selettore dal bitrate colorato.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-13 20:05:16 +02:00
co-authored by Cursor
parent bc2257efd2
commit f92ab3a813
65 changed files with 2745 additions and 147 deletions
@@ -5,18 +5,37 @@ import RTMPHaishinKit
import UIKit
import VideoToolbox
private struct BroadcastBitrateMonitor: StreamBitRateStrategy {
let mamimumVideoBitRate: Int = 0
let mamimumAudioBitRate: Int = 0
private final class AdaptiveBroadcastBitRateStrategy: StreamBitRateStrategy, @unchecked Sendable {
let controller: AdaptiveBitrateController
let onUpdate: @Sendable (Int) -> Void
var mamimumVideoBitRate: Int { controller.ceiling }
let mamimumAudioBitRate: Int = 0
init(controller: AdaptiveBitrateController, onUpdate: @escaping @Sendable (Int) -> Void) {
self.controller = controller
self.onUpdate = onUpdate
}
func adjustBitrate(_ event: NetworkMonitorEvent, stream: some StreamConvertible) async {
let next: Int?
switch event {
case .status(let report), .publishInsufficientBWOccured(let report):
case .status(let report):
let measuredBits = report.currentBytesOutPerSecond * 8
onUpdate(max(0, report.currentBytesOutPerSecond / 1024))
next = controller.onMeasured(measuredBitsPerSecond: measuredBits, congested: false)
case .publishInsufficientBWOccured(let report):
onUpdate(max(0, report.currentBytesOutPerSecond / 1024))
next = controller.onCongestion(measuredBitsPerSecond: report.currentBytesOutPerSecond * 8)
case .reset:
break
next = controller.resetToCeiling()
@unknown default:
next = nil
}
guard let bitrate = next else { return }
var videoSettings = await stream.videoSettings
guard videoSettings.bitRate != bitrate else { return }
videoSettings.bitRate = bitrate
try? await stream.setVideoSettings(videoSettings)
}
}
@@ -47,6 +66,7 @@ final class LiveBroadcastEngine: ObservableObject {
private var lastAppliedCaptureOrientation: AVCaptureVideoOrientation?
private var orientationDebounceTask: Task<Void, Never>?
private var rtmpSession: (any Session)?
private let adaptiveBitrate = AdaptiveBitrateController()
private static var rtmpFactoryRegistered = false
private static func ensureRTMPFactoryRegistered() async {
@@ -101,6 +121,7 @@ final class LiveBroadcastEngine: ObservableObject {
func preparePreview(config: BroadcastConfig) async throws {
self.config = config
adaptiveBitrate.configure(ceiling: config.videoBitrate, floor: config.minVideoBitrate)
publishPending = false
try await configurePipeline(config)
setPhase(.preview)
@@ -109,6 +130,7 @@ final class LiveBroadcastEngine: ObservableObject {
/// Configura camera/mixer e mette in coda il publish RTMP (eseguito quando l'anteprima è pronta).
func prepareForBroadcast(config: BroadcastConfig) async throws {
self.config = config
adaptiveBitrate.configure(ceiling: config.videoBitrate, floor: config.minVideoBitrate)
publishPending = true
try await configurePipeline(config)
setPhase(.connecting)
@@ -135,6 +157,7 @@ final class LiveBroadcastEngine: ObservableObject {
func resumeBroadcast(config: BroadcastConfig) async throws {
self.config = config
adaptiveBitrate.configure(ceiling: config.videoBitrate, floor: config.minVideoBitrate)
reconnectAttempts = 0
reconnectTask?.cancel()
reconnectTask = nil
@@ -179,18 +202,40 @@ final class LiveBroadcastEngine: ObservableObject {
await stopBroadcast()
}
/// Adattamento termico: aggiorna profilo video senza interrompere la sessione RTMP attiva.
/// Adattamento termico: aggiorna il soffitto ABR e l'FPS senza interrompere la sessione RTMP.
func applyThermalProfile(videoBitrate: Int, fps: Int) async {
guard var current = config else { return }
guard current.videoBitrate != videoBitrate || current.fps != fps else { return }
adaptiveBitrate.setCeiling(videoBitrate)
let applied = adaptiveBitrate.setFloor(current.minVideoBitrate)
guard current.videoBitrate != videoBitrate || current.fps != fps else {
await applyEncoderBitrate(applied)
return
}
current.videoBitrate = videoBitrate
current.fps = fps
config = current
if pipelineConfigured, let stream = await rtmpSession?.stream {
try? await applyCodecSettings(to: stream, config: current)
try? await applyCodecSettings(to: stream, config: current, videoBitrate: applied)
}
}
func setMinQualityFloor(_ floorBitrate: Int, ceiling: Int? = nil) async {
if var current = config {
if let ceiling { current.videoBitrate = ceiling }
current.minVideoBitrate = floorBitrate
config = current
}
await applyEncoderBitrate(adaptiveBitrate.setFloor(floorBitrate))
}
private func applyEncoderBitrate(_ bitrate: Int) async {
guard pipelineConfigured, let stream = await rtmpSession?.stream else { return }
var videoSettings = await stream.videoSettings
guard videoSettings.bitRate != bitrate else { return }
videoSettings.bitRate = bitrate
try? await stream.setVideoSettings(videoSettings)
}
private func configurePipeline(_ config: BroadcastConfig) async throws {
guard AVCaptureDevice.authorizationStatus(for: .video) == .authorized,
AVCaptureDevice.authorizationStatus(for: .audio) == .authorized else {
@@ -316,7 +361,7 @@ final class LiveBroadcastEngine: ObservableObject {
let stream = await session.stream
try await applyCodecSettings(to: stream, config: config)
await stream.setBitRateStrategy(BroadcastBitrateMonitor { [weak self] kbps in
await stream.setBitRateStrategy(AdaptiveBroadcastBitRateStrategy(controller: adaptiveBitrate) { [weak self] kbps in
Task { @MainActor in
guard let self else { return }
self.metrics = BroadcastMetrics(
@@ -349,10 +394,10 @@ final class LiveBroadcastEngine: ObservableObject {
setPhase(.live)
}
private func applyCodecSettings(to stream: any StreamConvertible, config: BroadcastConfig) async throws {
private func applyCodecSettings(to stream: any StreamConvertible, config: BroadcastConfig, videoBitrate: Int? = nil) async throws {
var videoSettings = await stream.videoSettings
videoSettings.videoSize = .init(width: config.width, height: config.height)
videoSettings.bitRate = config.videoBitrate
videoSettings.bitRate = videoBitrate ?? adaptiveBitrate.currentBitrate
videoSettings.maxKeyFrameIntervalDuration = 2
videoSettings.profileLevel = config.height > 720
? (kVTProfileLevel_H264_Baseline_4_1 as String)