Merge branch 'feature/adaptive-bitrate' into collaudo
Integra bitrate adattivo e telemetria, con il mute audio come icona nella toolbar della regia. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +67,7 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
private var orientationDebounceTask: Task<Void, Never>?
|
||||
private var rtmpSession: (any Session)?
|
||||
private var audioMuted = false
|
||||
private let adaptiveBitrate = AdaptiveBitrateController()
|
||||
private static var rtmpFactoryRegistered = false
|
||||
|
||||
private static func ensureRTMPFactoryRegistered() async {
|
||||
@@ -102,6 +122,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)
|
||||
@@ -110,6 +131,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)
|
||||
@@ -136,6 +158,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
|
||||
@@ -189,18 +212,40 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
emitMetrics()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -327,7 +372,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(
|
||||
@@ -361,10 +406,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)
|
||||
|
||||
Reference in New Issue
Block a user