2020IMPORTS = """import androidx.compose.foundation.layout.fillMaxSize
2121import androidx.compose.ui.Modifier
2222import androidx.compose.ui.awt.ComposePanel
23+ import com.nuvio.app.core.ui.NuvioTheme
2324import com.nuvio.app.features.player.PlatformPlayerSurface
2425import com.nuvio.app.features.player.PlayerControlsState
2526import com.nuvio.app.features.player.PlayerPlaybackSnapshot
4546 val failureStage: String,
4647 val durationSeconds: Double?,
4748 val positionSeconds: Double?,
49+ val exceptionChain: String = "",
4850 )
4951
52+ private fun desktopThrowableChain(error: Throwable): Pair<Throwable, String> {
53+ var current = error
54+ val parts = mutableListOf<String>()
55+ repeat(8) {
56+ val name = current::class.qualifiedName.orEmpty().ifBlank { current.javaClass.name }
57+ val message = current.message.orEmpty()
58+ .replace(Regex("https?://\\S+", RegexOption.IGNORE_CASE), "<url>")
59+ .replace(Regex("(?i)(authorization|cookie|token|secret)\\s*[:=]\\s*\\S+"), "$1=<redacted>")
60+ .replace(Regex("\\s+"), " ")
61+ .take(180)
62+ parts += if (message.isBlank()) name else "$name:$message"
63+ val next = when (current) {
64+ is java.lang.reflect.InvocationTargetException -> current.targetException ?: current.cause
65+ else -> current.cause
66+ }
67+ if (next == null || next === current) {
68+ return current to parts.joinToString(" -> ").take(420)
69+ }
70+ current = next
71+ }
72+ return current to parts.joinToString(" -> ").take(420)
73+ }
74+
5075 private fun captureDesktopPhase(phase: String, fixtureSlug: String) {
5176 val safe = phase.replace(Regex("[^A-Za-z0-9_.-]+"), "-").trim('-').ifBlank { "phase" }
5277 val dir = File(workspace, "native-evidence/desktop/${System.getProperty("os.name").lowercase().replace(Regex("[^a-z0-9]+"), "-")}/$fixtureSlug")
83108 frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
84109 frameRef.set(frame)
85110 panel.setContent {
86- // Production entry point. Provider output enters Nuvio unchanged;
87- // Nuvio's own production surface decides sanitation, settings,
88- // native bridge and playback behavior exactly as the app does.
89- PlatformPlayerSurface(
90- sourceUrl = url,
91- sourceHeaders = headers.orEmpty(),
92- sourceResponseHeaders = emptyMap(),
93- externalSubtitles = emptyList(),
94- streamType = streamType,
95- useYoutubeChunkedPlayback = false,
96- modifier = Modifier.fillMaxSize(),
97- playWhenReady = true,
98- initialPositionMs = 0L,
99- initialPositionRequestKey = "niakvio-native-reader",
100- resizeMode = PlayerResizeMode.Fit,
101- useNativeController = true,
102- playerControlsState = PlayerControlsState(),
103- onControllerReady = { _ -> },
104- onSnapshot = { snapshot ->
105- latestSnapshot.set(snapshot)
106- if (snapshot.isEnded || (!snapshot.isLoading && (snapshot.isPlaying || snapshot.positionMs > 0L || snapshot.durationMs > 0L))) {
107- terminal.countDown()
108- }
109- },
110- onError = { message ->
111- if (!message.isNullOrBlank()) {
112- errorRef.compareAndSet(null, message)
113- terminal.countDown()
114- }
115- },
116- )
111+ // PlatformPlayerSurface assumes the same production composition
112+ // locals as a normal Nuvio app screen. In particular the current
113+ // Desktop implementation reads LocalNuvioPlatformDensity, whose
114+ // deliberate default throws outside NuvioTheme. Keep the real
115+ // theme around the real surface instead of inventing test values.
116+ NuvioTheme {
117+ PlatformPlayerSurface(
118+ sourceUrl = url,
119+ sourceHeaders = headers.orEmpty(),
120+ sourceResponseHeaders = emptyMap(),
121+ externalSubtitles = emptyList(),
122+ streamType = streamType,
123+ useYoutubeChunkedPlayback = false,
124+ modifier = Modifier.fillMaxSize(),
125+ playWhenReady = true,
126+ initialPositionMs = 0L,
127+ initialPositionRequestKey = "niakvio-native-reader",
128+ resizeMode = PlayerResizeMode.Fit,
129+ useNativeController = true,
130+ playerControlsState = PlayerControlsState(),
131+ onControllerReady = { _ -> },
132+ onSnapshot = { snapshot ->
133+ latestSnapshot.set(snapshot)
134+ if (snapshot.isEnded || (!snapshot.isLoading && (snapshot.isPlaying || snapshot.positionMs > 0L || snapshot.durationMs > 0L))) {
135+ terminal.countDown()
136+ }
137+ },
138+ onError = { message ->
139+ if (!message.isNullOrBlank()) {
140+ errorRef.compareAndSet(null, message)
141+ terminal.countDown()
142+ }
143+ },
144+ )
145+ }
117146 }
118147 frame.isVisible = true
119148 }
145174 }
146175 return DesktopNativePlayerProbe("timeout", "NuvioDesktopProductionPlayer", "READER_TIMEOUT", 0, "timeout", durationSeconds, positionSeconds)
147176 } catch (error: Throwable) {
177+ val (root, chain) = desktopThrowableChain(error)
178+ val rootMessage = root.message.orEmpty().ifBlank { "PLAYER_SETUP" }
148179 return DesktopNativePlayerProbe(
149- "error", error ::class.qualifiedName.orEmpty(),
150- (error.message ?: "PLAYER_SETUP") .replace(Regex("\\s+"), "_").take(160),
151- 0, "player_setup", null, null,
180+ "error", root ::class.qualifiedName.orEmpty().ifBlank { root.javaClass.name } ,
181+ rootMessage .replace(Regex("\\s+"), "_").take(160),
182+ 0, "player_setup", null, null, chain,
152183 )
153184 } finally {
154185 runCatching { SwingUtilities.invokeAndWait { frameRef.getAndSet(null)?.dispose() } }
@@ -198,7 +229,7 @@ def augment(path: Path, expected_minutes: int, stream_scope: str) -> None:
198229 emit("FIELD_NATIVE_PLAYER_BEGIN client=desktop fixture=$fixtureSlug provider64=${{b64(provider.id)}} request_type=$requestMediaType route_mode=$routeMode index=$index entry=PlatformPlayerSurface")
199230 captureDesktopPhase("player-start", fixtureSlug)
200231 val reader = probeDesktopProductionPlayer(row.url, row.headers, row.type, { expected_minutes } )
201- emit("FIELD_NATIVE_PLAYER client=desktop fixture=$fixtureSlug provider64=${{b64(provider.id)}} request_type=$requestMediaType route_mode=$routeMode index=$index state=${{reader.state}} engine=nuvio-production-desktop http_status=${{reader.httpStatus}} failure_stage=${{reader.failureStage}} duration_seconds=${{reader.durationSeconds ?: 0.0}} host64=${{b64(hostOnly(row.url))}} error_class64=${{b64(reader.errorClass)}} error_code64=${{b64(reader.errorCode)}} exception_chain64=${{b64("" )}} response_header_names64=${{b64("")}} load_bytes=0 load_duration_ms=0 media_data_type=-1 track_type=-1")
232+ emit("FIELD_NATIVE_PLAYER client=desktop fixture=$fixtureSlug provider64=${{b64(provider.id)}} request_type=$requestMediaType route_mode=$routeMode index=$index state=${{reader.state}} engine=nuvio-production-desktop http_status=${{reader.httpStatus}} failure_stage=${{reader.failureStage}} duration_seconds=${{reader.durationSeconds ?: 0.0}} host64=${{b64(hostOnly(row.url))}} error_class64=${{b64(reader.errorClass)}} error_code64=${{b64(reader.errorCode)}} exception_chain64=${{b64(reader.exceptionChain )}} response_header_names64=${{b64("")}} load_bytes=0 load_duration_ms=0 media_data_type=-1 track_type=-1")
202233 captureDesktopPhase("player-result", fixtureSlug)
203234 // Independent transport diagnostics run only after the production
204235 // player has reached a terminal observation for this source.
@@ -222,7 +253,7 @@ def augment(path: Path, expected_minutes: int, stream_scope: str) -> None:
222253 print (
223254 f"FIELD_NATIVE_DESKTOP_READER_AUGMENTED source={ path } streams={ stream_scope } "
224255 f"expected_minutes={ expected_minutes } timeout_ms={ reader_timeout_ms } entry=PlatformPlayerSurface "
225- f"ci_mode={ 'pr-bounded' if pr_bounded else 'deep' } "
256+ f"ci_mode={ 'pr-bounded' if pr_bounded else 'deep' } theme=NuvioTheme "
226257 )
227258
228259
0 commit comments