feat(android): HDR toggle, video-feed stats, landscape lock
apple / swift (push) Successful in 1m6s
android / android (push) Successful in 5m14s
ci / web (push) Successful in 49s
apple / screenshots (push) Successful in 5m19s
ci / docs-site (push) Successful in 1m0s
ci / bench (push) Successful in 4m36s
ci / rust (push) Successful in 13m31s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 40s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 10m9s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m50s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m19s
docker / deploy-docs (push) Failing after 1m4s
apple / swift (push) Successful in 1m6s
android / android (push) Successful in 5m14s
ci / web (push) Successful in 49s
apple / screenshots (push) Successful in 5m19s
ci / docs-site (push) Successful in 1m0s
ci / bench (push) Successful in 4m36s
ci / rust (push) Successful in 13m31s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 40s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 10m9s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m50s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m19s
docker / deploy-docs (push) Failing after 1m4s
- HDR toggle in Settings → Display. Persisted (hdr_enabled, default on); the host is advertised HDR only when the toggle is on AND the panel can present HDR10 (displaySupportsHdr), so SDR panels never get PQ they'd mis-tone-map. The toggle is disabled/greyed on non-HDR displays (ToggleRow gained `enabled`). - Extend the stats HUD with a video-feed line, e.g. "HEVC · 10-bit · HDR (BT.2020 PQ) · 4:2:0". nativeVideoStats now returns 14 doubles (appends bitDepth, CICP primaries/transfer, chroma_format_idc from the negotiated Welcome); older/shorter layouts just omit the line. - Lock the stream to landscape while streaming (SENSOR_LANDSCAPE), restoring the prior orientation on exit. The activity declares configChanges=orientation, so it re-lays-out in place with no stream restart; harmless no-op on TV. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -175,9 +175,9 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) {
|
|||||||
status = "Connecting to $targetHost:$targetPort…"
|
status = "Connecting to $targetHost:$targetPort…"
|
||||||
discovery.stop() // free the Wi-Fi radio before the stream session
|
discovery.stop() // free the Wi-Fi radio before the stream session
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// Advertise HDR only when this device's display can present it (else the host sends a
|
// Advertise HDR only when the user enabled it AND this device's display can present it
|
||||||
// proper SDR stream rather than PQ the panel would mis-tone-map).
|
// (else the host sends a proper SDR stream rather than PQ the panel would mis-tone-map).
|
||||||
val hdrEnabled = displaySupportsHdr(context)
|
val hdrEnabled = settings.hdrEnabled && displaySupportsHdr(context)
|
||||||
// "Automatic" resolves to a concrete pad type from the connected controller's VID/PID
|
// "Automatic" resolves to a concrete pad type from the connected controller's VID/PID
|
||||||
// (Android exposes no controller-type enum) — parity with the Linux/Apple clients. An
|
// (Android exposes no controller-type enum) — parity with the Linux/Apple clients. An
|
||||||
// explicit choice is passed through unchanged.
|
// explicit choice is passed through unchanged.
|
||||||
@@ -224,7 +224,7 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) {
|
|||||||
status = null
|
status = null
|
||||||
discovery.stop() // free the Wi-Fi radio before the (parked) stream session
|
discovery.stop() // free the Wi-Fi radio before the (parked) stream session
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val hdrEnabled = displaySupportsHdr(context)
|
val hdrEnabled = settings.hdrEnabled && displaySupportsHdr(context)
|
||||||
val gamepadPref = Gamepad.resolvePref(settings.gamepad)
|
val gamepadPref = Gamepad.resolvePref(settings.gamepad)
|
||||||
// Pin the advertised fingerprint for a discovered host (defence against an impostor while
|
// Pin the advertised fingerprint for a discovered host (defence against an impostor while
|
||||||
// we wait); a manually-typed host has none, so trust-on-first-use.
|
// we wait); a manually-typed host has none, so trust-on-first-use.
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ data class Settings(
|
|||||||
val height: Int = 0,
|
val height: Int = 0,
|
||||||
val hz: Int = 0,
|
val hz: Int = 0,
|
||||||
val bitrateKbps: Int = 0,
|
val bitrateKbps: Int = 0,
|
||||||
|
/**
|
||||||
|
* Advertise HDR (10-bit BT.2020 PQ) to the host. Default on, but only *effective* on a panel that
|
||||||
|
* can actually present HDR10 (see [displaySupportsHdr]) — on an SDR display HDR is never
|
||||||
|
* advertised regardless, so the host sends a proper 8-bit BT.709 stream rather than PQ the panel
|
||||||
|
* would mis-tone-map. Turning this off forces SDR even on a capable panel.
|
||||||
|
*/
|
||||||
|
val hdrEnabled: Boolean = true,
|
||||||
val compositor: Int = 0,
|
val compositor: Int = 0,
|
||||||
val gamepad: Int = 0,
|
val gamepad: Int = 0,
|
||||||
/** Requested audio channel count: 2 (stereo), 6 (5.1) or 8 (7.1). The host clamps to what it
|
/** Requested audio channel count: 2 (stereo), 6 (5.1) or 8 (7.1). The host clamps to what it
|
||||||
@@ -40,6 +47,7 @@ class SettingsStore(context: Context) {
|
|||||||
height = prefs.getInt(K_H, 0),
|
height = prefs.getInt(K_H, 0),
|
||||||
hz = prefs.getInt(K_HZ, 0),
|
hz = prefs.getInt(K_HZ, 0),
|
||||||
bitrateKbps = prefs.getInt(K_BITRATE, 0),
|
bitrateKbps = prefs.getInt(K_BITRATE, 0),
|
||||||
|
hdrEnabled = prefs.getBoolean(K_HDR, true),
|
||||||
compositor = prefs.getInt(K_COMPOSITOR, 0),
|
compositor = prefs.getInt(K_COMPOSITOR, 0),
|
||||||
gamepad = prefs.getInt(K_GAMEPAD, 0),
|
gamepad = prefs.getInt(K_GAMEPAD, 0),
|
||||||
audioChannels = prefs.getInt(K_AUDIO_CH, 2),
|
audioChannels = prefs.getInt(K_AUDIO_CH, 2),
|
||||||
@@ -54,6 +62,7 @@ class SettingsStore(context: Context) {
|
|||||||
.putInt(K_H, s.height)
|
.putInt(K_H, s.height)
|
||||||
.putInt(K_HZ, s.hz)
|
.putInt(K_HZ, s.hz)
|
||||||
.putInt(K_BITRATE, s.bitrateKbps)
|
.putInt(K_BITRATE, s.bitrateKbps)
|
||||||
|
.putBoolean(K_HDR, s.hdrEnabled)
|
||||||
.putInt(K_COMPOSITOR, s.compositor)
|
.putInt(K_COMPOSITOR, s.compositor)
|
||||||
.putInt(K_GAMEPAD, s.gamepad)
|
.putInt(K_GAMEPAD, s.gamepad)
|
||||||
.putInt(K_AUDIO_CH, s.audioChannels)
|
.putInt(K_AUDIO_CH, s.audioChannels)
|
||||||
@@ -68,6 +77,7 @@ class SettingsStore(context: Context) {
|
|||||||
const val K_H = "height"
|
const val K_H = "height"
|
||||||
const val K_HZ = "hz"
|
const val K_HZ = "hz"
|
||||||
const val K_BITRATE = "bitrate_kbps"
|
const val K_BITRATE = "bitrate_kbps"
|
||||||
|
const val K_HDR = "hdr_enabled"
|
||||||
const val K_COMPOSITOR = "compositor"
|
const val K_COMPOSITOR = "compositor"
|
||||||
const val K_GAMEPAD = "gamepad"
|
const val K_GAMEPAD = "gamepad"
|
||||||
const val K_AUDIO_CH = "audio_channels"
|
const val K_AUDIO_CH = "audio_channels"
|
||||||
|
|||||||
@@ -94,6 +94,22 @@ fun SettingsScreen(initial: Settings, onChange: (Settings) -> Unit, onBack: () -
|
|||||||
options = BITRATE_OPTIONS,
|
options = BITRATE_OPTIONS,
|
||||||
selected = s.bitrateKbps,
|
selected = s.bitrateKbps,
|
||||||
) { kbps -> update(s.copy(bitrateKbps = kbps)) }
|
) { kbps -> update(s.copy(bitrateKbps = kbps)) }
|
||||||
|
|
||||||
|
// HDR is only meaningful on a panel that can present HDR10; on an SDR display the toggle
|
||||||
|
// is disabled (and HDR is never advertised regardless) so the host doesn't send PQ the
|
||||||
|
// panel would mis-tone-map. The capability is fixed for the device, so read it once.
|
||||||
|
val hdrCapable = remember { displaySupportsHdr(context) }
|
||||||
|
ToggleRow(
|
||||||
|
title = "HDR",
|
||||||
|
subtitle = if (hdrCapable) {
|
||||||
|
"Stream 10-bit HDR (BT.2020 PQ) when the host supports it"
|
||||||
|
} else {
|
||||||
|
"This display can't present HDR10 — streams stay SDR"
|
||||||
|
},
|
||||||
|
checked = s.hdrEnabled && hdrCapable,
|
||||||
|
enabled = hdrCapable,
|
||||||
|
onCheckedChange = { on -> update(s.copy(hdrEnabled = on)) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsGroup("Host") {
|
SettingsGroup("Host") {
|
||||||
@@ -181,24 +197,31 @@ private fun SettingsGroup(title: String, content: @Composable ColumnScope.() ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A title + subtitle on the left, a Switch on the right. */
|
/** A title + subtitle on the left, a Switch on the right. [enabled] greys out the whole row. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun ToggleRow(
|
private fun ToggleRow(
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
|
// Dim the labels when disabled so the row reads as inactive (the Switch dims itself).
|
||||||
|
val labelAlpha = if (enabled) 1f else 0.38f
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(title, style = MaterialTheme.typography.bodyLarge)
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = labelAlpha),
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = labelAlpha),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.unom.punktfunk
|
package io.unom.punktfunk
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
@@ -102,6 +103,13 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
|||||||
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
it.hide(WindowInsetsCompat.Type.systemBars())
|
it.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
}
|
}
|
||||||
|
// Lock to landscape while streaming — the host streams a landscape desktop, so pin the device
|
||||||
|
// there (either landscape direction is fine) and stop it rotating to portrait mid-session. The
|
||||||
|
// activity declares configChanges=orientation, so this re-lays out the surface in place without
|
||||||
|
// recreating the activity (no stream restart). On TV (fixed landscape) it's a harmless no-op.
|
||||||
|
// The prior request is captured and restored on the way out.
|
||||||
|
val priorOrientation = activity?.requestedOrientation
|
||||||
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
activity?.streamHandle = handle // route hardware keys to this session
|
activity?.streamHandle = handle // route hardware keys to this session
|
||||||
activity?.axisMapper = Gamepad.AxisMapper(handle) // route joystick axes
|
activity?.axisMapper = Gamepad.AxisMapper(handle) // route joystick axes
|
||||||
// Host→client feedback (rumble + DualSense lightbar/LEDs); poll threads stopped before close.
|
// Host→client feedback (rumble + DualSense lightbar/LEDs); poll threads stopped before close.
|
||||||
@@ -114,6 +122,9 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
|||||||
activity?.streamHandle = 0L
|
activity?.streamHandle = 0L
|
||||||
controller?.show(WindowInsetsCompat.Type.systemBars())
|
controller?.show(WindowInsetsCompat.Type.systemBars())
|
||||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
// Release the landscape lock so the rest of the app follows the device/system again.
|
||||||
|
activity?.requestedOrientation =
|
||||||
|
priorOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
// Leaving the stream: stop the mic + audio + decode threads and tear down the session.
|
// Leaving the stream: stop the mic + audio + decode threads and tear down the session.
|
||||||
NativeBridge.nativeStopMic(handle)
|
NativeBridge.nativeStopMic(handle)
|
||||||
NativeBridge.nativeStopAudio(handle)
|
NativeBridge.nativeStopAudio(handle)
|
||||||
@@ -314,9 +325,11 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The live stats overlay — mirrors the Apple client's HUD. Reads the 10-double layout from
|
* The live stats overlay — mirrors the Apple client's HUD. Reads the 14-double layout from
|
||||||
* [NativeBridge.nativeVideoStats]:
|
* [NativeBridge.nativeVideoStats]:
|
||||||
* `[fps, mbps, latP50Ms, latP95Ms, latValid, skew, w, h, hz, dropped]`.
|
* `[fps, mbps, latP50Ms, latP95Ms, latValid, skew, w, h, hz, dropped, bitDepth, colorPrimaries,
|
||||||
|
* colorTransfer, chromaFormatIdc]`. The trailing four (present on a current native lib) describe the
|
||||||
|
* negotiated video feed and render as a codec/depth/colour/chroma line; older layouts just omit it.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun StatsOverlay(s: DoubleArray, modifier: Modifier = Modifier) {
|
internal fun StatsOverlay(s: DoubleArray, modifier: Modifier = Modifier) {
|
||||||
@@ -338,6 +351,14 @@ internal fun StatsOverlay(s: DoubleArray, modifier: Modifier = Modifier) {
|
|||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
)
|
)
|
||||||
|
videoFeedLine(s)?.let { feed ->
|
||||||
|
Text(
|
||||||
|
feed,
|
||||||
|
color = Color.White,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (latValid) {
|
if (latValid) {
|
||||||
val tag = if (skew) "" else " (same-host)"
|
val tag = if (skew) "" else " (same-host)"
|
||||||
Text(
|
Text(
|
||||||
@@ -357,3 +378,31 @@ internal fun StatsOverlay(s: DoubleArray, modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the negotiated video-feed descriptor from the trailing four stats doubles
|
||||||
|
* `[bitDepth, colorPrimaries, colorTransfer, chromaFormatIdc]`, e.g.
|
||||||
|
* `HEVC · 10-bit · HDR (BT.2020 PQ) · 4:2:0`. Returns `null` on a pre-video-feed layout (< 14 doubles)
|
||||||
|
* so the overlay simply omits the line. The codes are CICP / H.273: transfer 16 = PQ, 18 = HLG (else
|
||||||
|
* SDR); primaries 9 = BT.2020, 1 = BT.709; chroma_format_idc 1 = 4:2:0, 2 = 4:2:2, 3 = 4:4:4. The
|
||||||
|
* Android decoder is always HEVC (`video/hevc`).
|
||||||
|
*/
|
||||||
|
private fun videoFeedLine(s: DoubleArray): String? {
|
||||||
|
if (s.size < 14) return null
|
||||||
|
val bitDepth = s[10].toInt()
|
||||||
|
val primaries = s[11].toInt()
|
||||||
|
val transfer = s[12].toInt()
|
||||||
|
val chromaIdc = s[13].toInt()
|
||||||
|
val depthLabel = if (bitDepth > 0) "$bitDepth-bit" else "8-bit"
|
||||||
|
val (dynamicRange, colorSpace) = when (transfer) {
|
||||||
|
16 -> "HDR" to "BT.2020 PQ"
|
||||||
|
18 -> "HDR" to "BT.2020 HLG"
|
||||||
|
else -> "SDR" to if (primaries == 9) "BT.2020" else "BT.709"
|
||||||
|
}
|
||||||
|
val chromaLabel = when (chromaIdc) {
|
||||||
|
3 -> "4:4:4"
|
||||||
|
2 -> "4:2:2"
|
||||||
|
else -> "4:2:0"
|
||||||
|
}
|
||||||
|
return "HEVC · $depthLabel · $dynamicRange ($colorSpace) · $chromaLabel"
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,9 +186,11 @@ internal fun StreamScene() {
|
|||||||
Brush.linearGradient(listOf(Color(0xFF2A1E5C), Color(0xFF0E1B3D), Color(0xFF06122B))),
|
Brush.linearGradient(listOf(Color(0xFF2A1E5C), Color(0xFF0E1B3D), Color(0xFF06122B))),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
// [fps, mbps, latP50, latP95, latValid, skew, w, h, hz, dropped]
|
// [fps, mbps, latP50, latP95, latValid, skew, w, h, hz, dropped,
|
||||||
|
// bitDepth, colorPrimaries, colorTransfer, chromaFormatIdc] — the last four = a 10-bit
|
||||||
|
// BT.2020 PQ (HDR) 4:2:0 feed, so the HUD renders its video-feed line.
|
||||||
StatsOverlay(
|
StatsOverlay(
|
||||||
doubleArrayOf(238.0, 921.4, 1.3, 2.1, 1.0, 1.0, 5120.0, 1440.0, 240.0, 0.0),
|
doubleArrayOf(238.0, 921.4, 1.3, 2.1, 1.0, 1.0, 5120.0, 1440.0, 240.0, 0.0, 10.0, 9.0, 16.0, 1.0),
|
||||||
Modifier.align(Alignment.TopStart).padding(12.dp),
|
Modifier.align(Alignment.TopStart).padding(12.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,9 +103,12 @@ object NativeBridge {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Drain ~1 s of live decode stats for the on-stream HUD, or `null` when no decode thread runs.
|
* Drain ~1 s of live decode stats for the on-stream HUD, or `null` when no decode thread runs.
|
||||||
* Returns 10 doubles:
|
* Returns 14 doubles:
|
||||||
* `[fps, mbps, latP50Ms, latP95Ms, latValid, skewCorrected, width, height, refreshHz, framesDropped]`
|
* `[fps, mbps, latP50Ms, latP95Ms, latValid, skewCorrected, width, height, refreshHz, framesDropped,
|
||||||
* (the two flags are 1.0/0.0). Poll ~1 Hz; each call resets the measurement window.
|
* bitDepth, colorPrimaries, colorTransfer, chromaFormatIdc]`
|
||||||
|
* (the two flags are 1.0/0.0; the trailing four describe the negotiated video feed — bit depth
|
||||||
|
* 8/10, CICP primaries/transfer, and the HEVC chroma_format_idc 1=4:2:0 / 3=4:4:4). Poll ~1 Hz;
|
||||||
|
* each call resets the measurement window.
|
||||||
*/
|
*/
|
||||||
external fun nativeVideoStats(handle: Long): DoubleArray?
|
external fun nativeVideoStats(handle: Long): DoubleArray?
|
||||||
|
|
||||||
|
|||||||
@@ -409,11 +409,13 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopVideo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `NativeBridge.nativeVideoStats(handle): DoubleArray?` — drain ~1 s of decode stats for the HUD.
|
/// `NativeBridge.nativeVideoStats(handle): DoubleArray?` — drain ~1 s of decode stats for the HUD.
|
||||||
/// Returns 10 doubles
|
/// Returns 14 doubles
|
||||||
/// `[fps, mbps, latP50Ms, latP95Ms, latValid, skewCorrected, width, height, refreshHz, framesDropped]`
|
/// `[fps, mbps, latP50Ms, latP95Ms, latValid, skewCorrected, width, height, refreshHz, framesDropped,
|
||||||
/// (the two flags are 1.0/0.0), or `null` when no decode thread is running. Poll ~1 Hz from the UI;
|
/// bitDepth, colorPrimaries, colorTransfer, chromaFormatIdc]`
|
||||||
/// each call resets the measurement window. Not android-gated — pure `jni` + connector reads, so it
|
/// (the two flags are 1.0/0.0; the trailing four describe the negotiated video feed — see below), or
|
||||||
/// links on the host build too (Kotlin only ever calls it on device).
|
/// `null` when no decode thread is running. Poll ~1 Hz from the UI; each call resets the measurement
|
||||||
|
/// window. Not android-gated — pure `jni` + connector reads, so it links on the host build too
|
||||||
|
/// (Kotlin only ever calls it on device).
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats(
|
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
@@ -431,7 +433,8 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats(
|
|||||||
None => return std::ptr::null_mut(), // not streaming → no stats
|
None => return std::ptr::null_mut(), // not streaming → no stats
|
||||||
};
|
};
|
||||||
let mode = h.client.mode();
|
let mode = h.client.mode();
|
||||||
let buf: [f64; 10] = [
|
let color = h.client.color;
|
||||||
|
let buf: [f64; 14] = [
|
||||||
snap.fps,
|
snap.fps,
|
||||||
snap.mbps,
|
snap.mbps,
|
||||||
snap.lat_p50_ms,
|
snap.lat_p50_ms,
|
||||||
@@ -442,6 +445,14 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats(
|
|||||||
mode.height as f64,
|
mode.height as f64,
|
||||||
mode.refresh_hz as f64,
|
mode.refresh_hz as f64,
|
||||||
h.client.frames_dropped() as f64,
|
h.client.frames_dropped() as f64,
|
||||||
|
// Video-feed properties the host resolved at the handshake (Welcome): encode bit depth
|
||||||
|
// (8 / 10), the CICP colour primaries + transfer code points (Kotlin maps these to a
|
||||||
|
// colour-space / HDR label — transfer 16 = PQ, 18 = HLG ⇒ HDR), and the HEVC
|
||||||
|
// chroma_format_idc (1 = 4:2:0, 3 = 4:4:4). Static for the session unless renegotiated.
|
||||||
|
h.client.bit_depth as f64,
|
||||||
|
color.primaries as f64,
|
||||||
|
color.transfer as f64,
|
||||||
|
h.client.chroma_format as f64,
|
||||||
];
|
];
|
||||||
let arr = match env.new_double_array(buf.len() as jsize) {
|
let arr = match env.new_double_array(buf.len() as jsize) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
|
|||||||
Reference in New Issue
Block a user