Riduce reload MediaMTX in live e allinea ingest mobile al preset qualità.

Evita patch recording ripetute che distruggevano i muxer HLS (500 ops) e fa encoderare 1080p/720p con codec e bitrate coerenti al wizard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-21 22:17:18 +02:00
parent e20c2ae619
commit 7d5d66840f
15 changed files with 195 additions and 40 deletions

View File

@@ -166,6 +166,7 @@ data class StreamSessionDto(
@Json(name = "privacy_status") val privacyStatus: String? = null,
@Json(name = "target_bitrate") val targetBitrate: Int? = null,
@Json(name = "target_fps") val targetFps: Int? = null,
@Json(name = "quality_preset") val qualityPreset: String? = null,
@Json(name = "started_at") val startedAt: String? = null,
val score: ScoreStateDto? = null,
) {
@@ -183,6 +184,7 @@ data class StreamSessionDto(
privacyStatus = privacyStatus ?: "public",
targetBitrate = targetBitrate ?: 2_500_000,
targetFps = targetFps ?: 30,
qualityPreset = qualityPreset ?: "720p_30_2.5mbps",
startedAt = startedAt,
score = score?.toDomain(),
)

View File

@@ -125,6 +125,7 @@ data class StreamSession(
val privacyStatus: String = "public",
val targetBitrate: Int = 2_500_000,
val targetFps: Int = 30,
val qualityPreset: String = "720p_30_2.5mbps",
val startedAt: String? = null,
val score: ScoreState? = null,
) {

View File

@@ -286,16 +286,21 @@ class LiveBroadcastEngine(
if (stream.isStreaming) stream.stopStream()
if (stream.isOnPreview) stream.stopPreview()
val h264Level = if (cfg.height > 720) {
MediaCodecInfo.CodecProfileLevel.AVCLevel41
} else {
MediaCodecInfo.CodecProfileLevel.AVCLevel31
}
val prepared = runCatching {
stream.prepareVideo(
width = cfg.width,
height = cfg.height,
bitrate = cfg.videoBitrate,
fps = cfg.fps,
iFrameInterval = 1,
iFrameInterval = 2,
rotation = cfg.rotation,
profile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline,
level = MediaCodecInfo.CodecProfileLevel.AVCLevel31,
level = h264Level,
) && stream.prepareAudio(48_000, false, cfg.audioBitrate)
}.getOrElse {
Log.w(TAG, "preparePipeline: ${it.message}")

View File

@@ -0,0 +1,26 @@
package com.matchlivetv.match_live_tv.streaming
import com.matchlivetv.match_live_tv.domain.StreamSession
/** Risoluzione encoder allineata al preset qualità scelto in wizard. */
object StreamVideoPreset {
data class Dimensions(val width: Int, val height: Int)
fun dimensions(qualityPreset: String?): Dimensions =
if (qualityPreset?.startsWith("1080p") == true) {
Dimensions(1920, 1080)
} else {
Dimensions(1280, 720)
}
fun broadcastConfig(session: StreamSession, rtmpUrl: String): BroadcastConfig {
val dims = dimensions(session.qualityPreset)
return BroadcastConfig(
rtmpUrl = rtmpUrl,
width = dims.width,
height = dims.height,
videoBitrate = session.targetBitrate,
fps = session.targetFps,
)
}
}

View File

@@ -37,6 +37,7 @@ import com.matchlivetv.match_live_tv.domain.Match
import com.matchlivetv.match_live_tv.domain.MatchScoringRules
import com.matchlivetv.match_live_tv.domain.StreamSession
import com.matchlivetv.match_live_tv.streaming.BroadcastConfig
import com.matchlivetv.match_live_tv.streaming.StreamVideoPreset
import com.matchlivetv.match_live_tv.streaming.BroadcastPhase
import com.matchlivetv.match_live_tv.streaming.LivePreviewView
import com.matchlivetv.match_live_tv.streaming.overlay.CompactScoreboardState
@@ -83,13 +84,8 @@ fun BroadcastScreen(
scoringRules?.let { LiveScoreActions(it, container.scoreController, dialogHost) }
}
fun broadcastConfig(loaded: StreamSession): BroadcastConfig = BroadcastConfig(
rtmpUrl = loaded.rtmpIngestUrl.orEmpty(),
width = 1280,
height = 720,
videoBitrate = loaded.targetBitrate,
fps = loaded.targetFps,
)
fun broadcastConfig(loaded: StreamSession): BroadcastConfig =
StreamVideoPreset.broadcastConfig(loaded, loaded.rtmpIngestUrl.orEmpty())
suspend fun stopStreamPermanently() {
runCatching { container.sessionRepository.stopSession(sessionId) }

View File

@@ -51,6 +51,7 @@
81DB6B2543BC407EBB056C4B /* BroadcastScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985215153D0A44DC8B429731 /* BroadcastScreen.swift */; };
833558A9A0A043ECB8E146F6 /* ScoreActionDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3A1338FBA0411587BB5F4B /* ScoreActionDecodeTests.swift */; };
87DB3FBE122F444B87B9D73B /* LiveBroadcastEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8B7397C0BD4EC59851185D /* LiveBroadcastEngine.swift */; };
F1A2B3C4D5E6478990ABCDEF /* StreamVideoPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6478990ABCDEF /* StreamVideoPreset.swift */; };
8841513B4E1743E983E7D2C8 /* BroadcastControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF2832197EE409AA0B0334C /* BroadcastControlsOverlay.swift */; };
8C32CAC69405444DA4BB1E36 /* ScoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41CF197E5D543CE9A949497 /* ScoreController.swift */; };
8D7FCD0052674C0EB4F5C811 /* MatchesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63D6F174CE084074972CABF6 /* MatchesScreen.swift */; };
@@ -154,6 +155,7 @@
B8736A8A330D4815A3D56104 /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = "<group>"; };
BA288DB764E24438AE8826DC /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = "<group>"; };
BC8B7397C0BD4EC59851185D /* LiveBroadcastEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastEngine.swift; path = MatchLiveTv/Streaming/LiveBroadcastEngine.swift; sourceTree = "<group>"; };
A1B2C3D4E5F6478990ABCDEF /* StreamVideoPreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StreamVideoPreset.swift; path = MatchLiveTv/Streaming/StreamVideoPreset.swift; sourceTree = "<group>"; };
BCA50054BF694B9D82EA97ED /* BroadcastVideoOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastVideoOrientation.swift; path = MatchLiveTv/Streaming/BroadcastVideoOrientation.swift; sourceTree = "<group>"; };
BDC3D4B19E974898AA9CAC76 /* SponsorElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SponsorElement.swift; path = MatchLiveTv/Streaming/Overlay/SponsorElement.swift; sourceTree = "<group>"; };
BFE4871BCA51457CB720047B /* BroadcastOrientationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastOrientationPolicy.swift; path = MatchLiveTv/Streaming/BroadcastOrientationPolicy.swift; sourceTree = "<group>"; };
@@ -243,6 +245,7 @@
BCA50054BF694B9D82EA97ED /* BroadcastVideoOrientation.swift */,
A3F2049B408743AA82AF43F3 /* LiveBroadcastCoordinator.swift */,
BC8B7397C0BD4EC59851185D /* LiveBroadcastEngine.swift */,
A1B2C3D4E5F6478990ABCDEF /* StreamVideoPreset.swift */,
AC36378EDDDF44F29ACAD3C6 /* CompactScoreboardElement.swift */,
1B5BF42CCF934AC7A9516374 /* OverlayCanvasRenderer.swift */,
10BEC6633F9F446587F1818C /* OverlayLogoCache.swift */,
@@ -417,6 +420,7 @@
C1D2EA29B25C4B06B03B6FFE /* BroadcastVideoOrientation.swift in Sources */,
539D6877DF3C478E86200649 /* LiveBroadcastCoordinator.swift in Sources */,
87DB3FBE122F444B87B9D73B /* LiveBroadcastEngine.swift in Sources */,
F1A2B3C4D5E6478990ABCDEF /* StreamVideoPreset.swift in Sources */,
AA9BCB0DA5164D43BD7A43FA /* CompactScoreboardElement.swift in Sources */,
ED730C9B9068449E8D8AF6C8 /* OverlayCanvasRenderer.swift in Sources */,
8F6D3E3D61584BBA865AD2D7 /* OverlayLogoCache.swift in Sources */,

View File

@@ -173,6 +173,7 @@ struct StreamSessionDto: Decodable {
let privacyStatus: String?
let targetBitrate: Int?
let targetFps: Int?
let qualityPreset: String?
let startedAt: String?
let score: ScoreStateDto?
@@ -191,6 +192,7 @@ struct StreamSessionDto: Decodable {
privacyStatus: privacyStatus ?? "public",
targetBitrate: targetBitrate ?? 2_500_000,
targetFps: targetFps ?? 30,
qualityPreset: qualityPreset ?? "720p_30_2.5mbps",
startedAt: startedAt,
score: score?.toDomain()
)

View File

@@ -118,6 +118,7 @@ struct StreamSession: Identifiable, Equatable, Sendable {
let privacyStatus: String
let targetBitrate: Int
let targetFps: Int
let qualityPreset: String
let startedAt: String?
let score: ScoreState?

View File

@@ -334,7 +334,9 @@ final class LiveBroadcastEngine: ObservableObject {
videoSettings.videoSize = .init(width: config.width, height: config.height)
videoSettings.bitRate = config.videoBitrate
videoSettings.maxKeyFrameIntervalDuration = 2
videoSettings.profileLevel = kVTProfileLevel_H264_Baseline_3_1 as String
videoSettings.profileLevel = config.height > 720
? (kVTProfileLevel_H264_Baseline_4_1 as String)
: (kVTProfileLevel_H264_Baseline_3_1 as String)
videoSettings.expectedFrameRate = Double(config.fps)
try await stream.setVideoSettings(videoSettings)

View File

@@ -0,0 +1,27 @@
import Foundation
enum StreamVideoPreset {
struct Dimensions: Sendable {
let width: Int
let height: Int
}
static func dimensions(for qualityPreset: String?) -> Dimensions {
if qualityPreset?.hasPrefix("1080p") == true {
return Dimensions(width: 1920, height: 1080)
}
return Dimensions(width: 1280, height: 720)
}
static func broadcastConfig(for session: StreamSession, rtmpUrl: String) -> BroadcastConfig {
let dims = dimensions(for: session.qualityPreset)
return BroadcastConfig(
rtmpUrl: rtmpUrl,
width: dims.width,
height: dims.height,
videoBitrate: session.targetBitrate,
audioBitrate: 128_000,
fps: session.targetFps
)
}
}

View File

@@ -505,13 +505,9 @@ struct BroadcastScreen: View {
}
private func broadcastConfig(for loaded: StreamSession, rtmpUrl: String) -> BroadcastConfig {
BroadcastConfig(
rtmpUrl: RtmpIngestUrl.resolveForDevice(rtmpUrl),
width: 1280,
height: 720,
videoBitrate: loaded.targetBitrate,
audioBitrate: 128_000,
fps: loaded.targetFps
StreamVideoPreset.broadcastConfig(
for: loaded,
rtmpUrl: RtmpIngestUrl.resolveForDevice(rtmpUrl)
)
}