Skip to content

Commit 02a52c8

Browse files
committed
lab: drive NuvioDesktop production player surface
1 parent dbb6c2b commit 02a52c8

1 file changed

Lines changed: 97 additions & 74 deletions

File tree

scripts/augment_native_desktop_player.py

Lines changed: 97 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#!/usr/bin/env python3
2-
"""Augment a generated NuvioDesktop corpus test with the official native player.
2+
"""Augment a generated NuvioDesktop corpus test with the production player surface.
33
4-
This runs only on macOS/Windows. Linux NuvioDesktop intentionally uses a stub player
5-
and is therefore never accepted as native-reader proof.
4+
Human-UX invariant: provider output is handed to NuvioDesktop's real
5+
PlatformPlayerSurface. The lab does not construct NativePlayerController directly,
6+
does not sanitize/rewrite the stream itself and does not choose decoder/network
7+
settings. Whatever Nuvio's production player does with the source is the evidence.
68
"""
79
from __future__ import annotations
810

@@ -11,17 +13,23 @@
1113
import re
1214
from pathlib import Path
1315

14-
1516
IMPORT_ANCHOR = "import com.nuvio.app.features.plugins.runtime.PluginRuntime\n"
1617
TEST_ANCHOR = " @Test\n"
17-
18-
IMPORTS = """import com.nuvio.app.features.player.desktop.DesktopHostOs
19-
import com.nuvio.app.features.player.desktop.NativePlayerController
20-
import com.nuvio.app.features.player.desktop.NativePlayerHost
18+
DEFAULT_PR_STREAM_LIMIT = 2
19+
20+
IMPORTS = """import androidx.compose.foundation.layout.fillMaxSize
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.awt.ComposePanel
23+
import com.nuvio.app.features.player.PlatformPlayerSurface
24+
import com.nuvio.app.features.player.PlayerControlsState
25+
import com.nuvio.app.features.player.PlayerPlaybackSnapshot
26+
import com.nuvio.app.features.player.PlayerResizeMode
2127
import java.awt.BorderLayout
2228
import java.awt.Rectangle
2329
import java.awt.Robot
2430
import java.awt.Toolkit
31+
import java.util.concurrent.CountDownLatch
32+
import java.util.concurrent.TimeUnit
2533
import java.util.concurrent.atomic.AtomicReference
2634
import javax.imageio.ImageIO
2735
import javax.swing.JFrame
@@ -54,84 +62,95 @@
5462
}
5563
}
5664
57-
private fun probeDesktopNativePlayer(
65+
private fun probeDesktopProductionPlayer(
5866
url: String,
5967
headers: Map<String, String>?,
68+
streamType: String?,
6069
expectedDurationMinutes: Int,
6170
): DesktopNativePlayerProbe {
62-
if (DesktopHostOs.current != DesktopHostOs.MACOS && DesktopHostOs.current != DesktopHostOs.WINDOWS) {
63-
return DesktopNativePlayerProbe("unsupported", "DesktopHostOs", "LINUX_STUB", 0, "player_setup", null, null)
64-
}
6571
val frameRef = AtomicReference<JFrame?>(null)
66-
val controllerRef = AtomicReference<NativePlayerController?>(null)
72+
val latestSnapshot = AtomicReference<PlayerPlaybackSnapshot?>(null)
6773
val errorRef = AtomicReference<String?>(null)
74+
val terminal = CountDownLatch(1)
6875
try {
6976
SwingUtilities.invokeAndWait {
7077
val frame = JFrame("Nuvio Desktop native reader lab")
71-
val host = NativePlayerHost()
78+
val panel = ComposePanel()
7279
frame.layout = BorderLayout()
73-
frame.add(host, BorderLayout.CENTER)
80+
frame.add(panel, BorderLayout.CENTER)
7481
frame.setSize(960, 540)
7582
frame.setLocationRelativeTo(null)
76-
frame.isVisible = true
77-
val controller = NativePlayerController(host)
83+
frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
7884
frameRef.set(frame)
79-
controllerRef.set(controller)
80-
}
81-
val controller = controllerRef.get()
82-
?: return DesktopNativePlayerProbe("error", "NativePlayerController", "NO_CONTROLLER", 0, "player_setup", null, null)
83-
controller.attach(
84-
// Observational-purity contract: the lab cannot make a source easier
85-
// to play than Nuvio would receive it. URL and provider headers are
86-
// passed byte-for-byte/value-for-value to the official controller.
87-
sourceUrl = url,
88-
sourceHeaders = headers.orEmpty(),
89-
playWhenReady = true,
90-
initialPositionMs = 0L,
91-
decoderPriority = 1,
92-
nvidiaRtxSuperResolutionEnabled = false,
93-
onError = { message -> errorRef.compareAndSet(null, message ?: "native_player_error") },
94-
)
95-
val deadline = System.currentTimeMillis() + __READER_TIMEOUT_MS__L
96-
var lastDuration: Long = 0L
97-
var lastPosition: Long = 0L
98-
while (System.currentTimeMillis() < deadline) {
99-
errorRef.get()?.let { message ->
100-
return DesktopNativePlayerProbe(
101-
"error", "NativePlayer", message.replace(Regex("\\s+"), "_").take(120),
102-
0, "player", null, null,
85+
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+
},
103116
)
104117
}
105-
val snapshot = controller.snapshot()
106-
lastDuration = snapshot.durationMs.coerceAtLeast(0L)
107-
lastPosition = snapshot.positionMs.coerceAtLeast(0L)
108-
val durationSeconds = lastDuration.takeIf { it > 0 }?.div(1000.0)
109-
val expected = expectedDurationMinutes.takeIf { it > 0 }?.times(60.0)
110-
val shortMedia = durationSeconds != null && (
111-
durationSeconds < 60.0 || (expected != null && durationSeconds / expected < 0.55)
118+
frame.isVisible = true
119+
}
120+
121+
terminal.await(__READER_TIMEOUT_MS__L, TimeUnit.MILLISECONDS)
122+
val error = errorRef.get()
123+
val snapshot = latestSnapshot.get()
124+
if (!error.isNullOrBlank()) {
125+
return DesktopNativePlayerProbe(
126+
"error", "NuvioDesktopProductionPlayer",
127+
error.replace(Regex("\\s+"), "_").take(160),
128+
0, "player", null, null,
112129
)
113-
if (shortMedia) {
114-
return DesktopNativePlayerProbe("short_media", "", "", 0, "duration_identity", durationSeconds, lastPosition / 1000.0)
115-
}
116-
if (snapshot.isEnded) {
117-
return DesktopNativePlayerProbe("ended", "", "", 0, "none", durationSeconds, lastPosition / 1000.0)
118-
}
119-
if (!snapshot.isLoading && (snapshot.isPlaying || lastPosition > 0L || lastDuration > 0L)) {
120-
return DesktopNativePlayerProbe("ready", "", "", 0, "none", durationSeconds, lastPosition / 1000.0)
121-
}
122-
Thread.sleep(500L)
123130
}
124-
return DesktopNativePlayerProbe(
125-
"timeout", "NativePlayer", "READER_TIMEOUT", 0, "timeout",
126-
lastDuration.takeIf { it > 0 }?.div(1000.0),
127-
lastPosition.takeIf { it > 0 }?.div(1000.0),
131+
val durationSeconds = snapshot?.durationMs?.takeIf { it > 0L }?.div(1000.0)
132+
val positionSeconds = snapshot?.positionMs?.takeIf { it >= 0L }?.div(1000.0)
133+
val expected = expectedDurationMinutes.takeIf { it > 0 }?.times(60.0)
134+
val shortMedia = durationSeconds != null && (
135+
durationSeconds < 60.0 || (expected != null && durationSeconds / expected < 0.55)
128136
)
137+
if (shortMedia) {
138+
return DesktopNativePlayerProbe("short_media", "", "", 0, "duration_identity", durationSeconds, positionSeconds)
139+
}
140+
if (snapshot?.isEnded == true) {
141+
return DesktopNativePlayerProbe("ended", "", "", 0, "none", durationSeconds, positionSeconds)
142+
}
143+
if (snapshot != null && !snapshot.isLoading && (snapshot.isPlaying || snapshot.positionMs > 0L || snapshot.durationMs > 0L)) {
144+
return DesktopNativePlayerProbe("ready", "", "", 0, "none", durationSeconds, positionSeconds)
145+
}
146+
return DesktopNativePlayerProbe("timeout", "NuvioDesktopProductionPlayer", "READER_TIMEOUT", 0, "timeout", durationSeconds, positionSeconds)
129147
} catch (error: Throwable) {
130148
return DesktopNativePlayerProbe(
131-
"error", error::class.qualifiedName.orEmpty(), "PLAYER_SETUP", 0, "player_setup", null, null,
149+
"error", error::class.qualifiedName.orEmpty(),
150+
(error.message ?: "PLAYER_SETUP").replace(Regex("\\s+"), "_").take(160),
151+
0, "player_setup", null, null,
132152
)
133153
} finally {
134-
runCatching { controllerRef.getAndSet(null)?.dispose() }
135154
runCatching { SwingUtilities.invokeAndWait { frameRef.getAndSet(null)?.dispose() } }
136155
}
137156
}
@@ -147,20 +166,21 @@ def replace_once(text: str, old: str, new: str, label: str) -> str:
147166

148167
def augment(path: Path, expected_minutes: int, stream_scope: str) -> None:
149168
text = path.read_text(encoding="utf-8")
150-
if "DesktopNativePlayerProbe" in text:
169+
if "probeDesktopProductionPlayer" in text:
151170
return
152171
pr_bounded = os.environ.get("GITHUB_EVENT_NAME", "").strip().lower() == "pull_request"
153172
if pr_bounded and stream_scope == "all":
154-
# Match Android's bounded PR proof: two returned rows are enough to expose
155-
# the common "first source works, second source breaks" class without
156-
# turning every PR into the trusted exhaustive run.
157-
stream_scope = "2"
173+
configured = os.environ.get("NIAKVIO_PR_STREAM_LIMIT", str(DEFAULT_PR_STREAM_LIMIT)).strip()
174+
try:
175+
stream_scope = str(max(1, min(int(configured), 4)))
176+
except ValueError:
177+
stream_scope = str(DEFAULT_PR_STREAM_LIMIT)
158178
reader_timeout_ms = 12_000 if pr_bounded else 25_000
179+
159180
text = replace_once(text, IMPORT_ANCHOR, IMPORT_ANCHOR + IMPORTS, "imports")
160181
helpers = HELPERS.replace("__READER_TIMEOUT_MS__", str(reader_timeout_ms))
161182
text = replace_once(text, TEST_ANCHOR, helpers + "\n" + TEST_ANCHOR, "test")
162183

163-
# Make row evidence match the actual reader scope.
164184
if stream_scope == "all":
165185
text = re.sub(r"rows\.take\(\d+\)\.forEachIndexed", "rows.forEachIndexed", text)
166186
reader_iter = "rows.forEachIndexed"
@@ -175,11 +195,13 @@ def augment(path: Path, expected_minutes: int, stream_scope: str) -> None:
175195
r''' \}\n'''
176196
)
177197
replacement = f''' {reader_iter} {{ index, row ->
178-
emit("FIELD_NATIVE_PLAYER_BEGIN client=desktop fixture=$fixtureSlug provider64=${{b64(provider.id)}} request_type=$requestMediaType route_mode=$routeMode index=$index")
198+
emit("FIELD_NATIVE_PLAYER_BEGIN client=desktop fixture=$fixtureSlug provider64=${{b64(provider.id)}} request_type=$requestMediaType route_mode=$routeMode index=$index entry=PlatformPlayerSurface")
179199
captureDesktopPhase("player-start", fixtureSlug)
180-
val reader = probeDesktopNativePlayer(row.url, row.headers, {expected_minutes})
181-
emit("FIELD_NATIVE_PLAYER client=desktop fixture=$fixtureSlug provider64=${{b64(provider.id)}} request_type=$requestMediaType route_mode=$routeMode index=$index state=${{reader.state}} engine=native-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")
200+
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")
182202
captureDesktopPhase("player-result", fixtureSlug)
203+
// Independent transport diagnostics run only after the production
204+
// player has reached a terminal observation for this source.
183205
val transport = probeTransport(row.url, row.headers)
184206
emit("FIELD_NATIVE_TRANSPORT client=desktop fixture=$fixtureSlug provider64=${{b64(provider.id)}} request_type=$requestMediaType route_mode=$routeMode index=$index state=${{transport.state}} kind=${{transport.kind}} status=${{transport.status}} content_type64=${{b64(transport.contentType)}} extm3u=${{transport.extm3u}} duration_seconds=${{transport.durationSeconds ?: 0.0}} host64=${{b64(transport.host)}} media_hint64=${{b64(transport.mediaHint)}}")
185207
}}
@@ -199,7 +221,8 @@ def augment(path: Path, expected_minutes: int, stream_scope: str) -> None:
199221
path.write_text(text, encoding="utf-8")
200222
print(
201223
f"FIELD_NATIVE_DESKTOP_READER_AUGMENTED source={path} streams={stream_scope} "
202-
f"expected_minutes={expected_minutes} timeout_ms={reader_timeout_ms} ci_mode={'pr-bounded' if pr_bounded else 'deep'}"
224+
f"expected_minutes={expected_minutes} timeout_ms={reader_timeout_ms} entry=PlatformPlayerSurface "
225+
f"ci_mode={'pr-bounded' if pr_bounded else 'deep'}"
203226
)
204227

205228

0 commit comments

Comments
 (0)