feat(android): native display resolution + Settings screen
apple / swift (push) Successful in 53s
android / android (push) Failing after 1m15s
ci / rust (push) Failing after 43s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 30s
ci / bench (push) Successful in 1m43s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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 3s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m53s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m44s
deb / build-publish (push) Successful in 6m52s
docker / deploy-docs (push) Successful in 22s
apple / swift (push) Successful in 53s
android / android (push) Failing after 1m15s
ci / rust (push) Failing after 43s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 30s
ci / bench (push) Successful in 1m43s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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 3s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m53s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m44s
deb / build-publish (push) Successful in 6m52s
docker / deploy-docs (push) Successful in 22s
The connect mode was hardcoded to 720p60 — violating the "native client resolution, no scaling" invariant. Derive the device's real display mode (landscape, long edge = width) and add a Settings screen to tune the stream, mirroring the Linux/Apple clients. - crates/punktfunk-android: nativeConnect gains bitrateKbps + compositorPref + gamepadPref (CompositorPref/GamepadPref wire bytes via from_u8); these were hardcoded Auto/Auto/0. - app/Settings.kt: Settings (width/height/hz/bitrate/compositor/gamepad; 0 = native/auto) + a SharedPreferences store + nativeDisplayMode (Display.mode, landscape-swapped) + effectiveMode + the UI option tables. - app/SettingsScreen.kt: dropdowns for resolution / refresh / bitrate / compositor / controller. - MainActivity: App owns the settings + a Settings screen; ConnectScreen resolves the effective mode (Native = the display), shows it on the Connect button, and threads the prefs through nativeConnect. Mic + codec selection deferred (mic uplink isn't wired yet; the decoder is HEVC-only). Verified live (emulator pf_phone -> home-worker-2): default -> host mode=2400x1080@60 (the emulator's native display, was 720p); Settings 1920x1080 + 20 Mbps + DualSense -> host mode=1920x1080, requested_kbps=20000, gamepad=dualsense (host created a UHID DualSense). Settings persist across screens; pinned reconnect stays silent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -137,11 +137,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scaffold mode requested from the host (WxH@Hz). TODO: derive from the display. */
|
|
||||||
private val REQUEST_MODE = Triple(1280, 720, 60)
|
|
||||||
|
|
||||||
private sealed interface Screen {
|
private sealed interface Screen {
|
||||||
data object Connect : Screen
|
data object Connect : Screen
|
||||||
|
data object Settings : Screen
|
||||||
data class Stream(val handle: Long) : Screen
|
data class Stream(val handle: Long) : Screen
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,15 +161,27 @@ private data class PendingTrust(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun App() {
|
private fun App() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val settingsStore = remember { SettingsStore(context) }
|
||||||
|
var settings by remember { mutableStateOf(settingsStore.load()) }
|
||||||
var screen by remember { mutableStateOf<Screen>(Screen.Connect) }
|
var screen by remember { mutableStateOf<Screen>(Screen.Connect) }
|
||||||
when (val s = screen) {
|
when (val s = screen) {
|
||||||
Screen.Connect -> ConnectScreen(onConnected = { handle -> screen = Screen.Stream(handle) })
|
Screen.Connect -> ConnectScreen(
|
||||||
|
settings = settings,
|
||||||
|
onConnected = { handle -> screen = Screen.Stream(handle) },
|
||||||
|
onOpenSettings = { screen = Screen.Settings },
|
||||||
|
)
|
||||||
|
Screen.Settings -> SettingsScreen(
|
||||||
|
initial = settings,
|
||||||
|
onChange = { settings = it; settingsStore.save(it) },
|
||||||
|
onBack = { screen = Screen.Connect },
|
||||||
|
)
|
||||||
is Screen.Stream -> StreamScreen(s.handle, onDisconnect = { screen = Screen.Connect })
|
is Screen.Stream -> StreamScreen(s.handle, onDisconnect = { screen = Screen.Connect })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConnectScreen(onConnected: (Long) -> Unit) {
|
private fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit, onOpenSettings: () -> Unit) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var host by remember { mutableStateOf("") }
|
var host by remember { mutableStateOf("") }
|
||||||
@@ -179,7 +189,8 @@ private fun ConnectScreen(onConnected: (Long) -> Unit) {
|
|||||||
var connecting by remember { mutableStateOf(false) }
|
var connecting by remember { mutableStateOf(false) }
|
||||||
var status by remember { mutableStateOf<String?>(null) }
|
var status by remember { mutableStateOf<String?>(null) }
|
||||||
val abi = remember { runCatching { NativeBridge.abiVersion() }.getOrDefault(-1) }
|
val abi = remember { runCatching { NativeBridge.abiVersion() }.getOrDefault(-1) }
|
||||||
val (w, h, hz) = REQUEST_MODE
|
// The host streams at exactly this mode; "Native" settings resolve from the device display.
|
||||||
|
val (w, h, hz) = settings.effectiveMode(context)
|
||||||
|
|
||||||
// mDNS discovery scoped to this screen; NsdManager callbacks arrive on the main thread, so the
|
// mDNS discovery scoped to this screen; NsdManager callbacks arrive on the main thread, so the
|
||||||
// onChange callback can set Compose state directly. (Emulator SLIRP drops multicast → empty.)
|
// onChange callback can set Compose state directly. (Emulator SLIRP drops multicast → empty.)
|
||||||
@@ -225,6 +236,7 @@ private fun ConnectScreen(onConnected: (Long) -> Unit) {
|
|||||||
NativeBridge.nativeConnect(
|
NativeBridge.nativeConnect(
|
||||||
targetHost, targetPort, w, h, hz,
|
targetHost, targetPort, w, h, hz,
|
||||||
id.certPem, id.privateKeyPem, pinHex ?: "",
|
id.certPem, id.privateKeyPem, pinHex ?: "",
|
||||||
|
settings.bitrateKbps, settings.compositor, settings.gamepad,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
connecting = false
|
connecting = false
|
||||||
@@ -311,6 +323,7 @@ private fun ConnectScreen(onConnected: (Long) -> Unit) {
|
|||||||
enabled = !connecting && host.isNotBlank() && port.isNotBlank(),
|
enabled = !connecting && host.isNotBlank() && port.isNotBlank(),
|
||||||
onClick = { connect(host.trim(), port.toInt()) },
|
onClick = { connect(host.trim(), port.toInt()) },
|
||||||
) { Text(if (connecting) "Connecting…" else "Connect ($w×$h@$hz)") }
|
) { Text(if (connecting) "Connecting…" else "Connect ($w×$h@$hz)") }
|
||||||
|
TextButton(enabled = !connecting, onClick = onOpenSettings) { Text("Settings") }
|
||||||
status?.let {
|
status?.let {
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text(it, style = MaterialTheme.typography.bodySmall)
|
Text(it, style = MaterialTheme.typography.bodySmall)
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package io.unom.punktfunk
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-tunable stream settings, persisted in `SharedPreferences`. A `0` resolution/refresh means
|
||||||
|
* "native display mode" (resolved at connect time from [nativeDisplayMode]); `0` bitrate means the
|
||||||
|
* host's default. [compositor]/[gamepad] are the `CompositorPref`/`GamepadPref` wire bytes the host
|
||||||
|
* understands (0 = Auto). Mirrors the Linux/Apple clients' settings.
|
||||||
|
*/
|
||||||
|
data class Settings(
|
||||||
|
val width: Int = 0,
|
||||||
|
val height: Int = 0,
|
||||||
|
val hz: Int = 0,
|
||||||
|
val bitrateKbps: Int = 0,
|
||||||
|
val compositor: Int = 0,
|
||||||
|
val gamepad: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Loads/saves [Settings] in the app-private `punktfunk_settings` prefs. */
|
||||||
|
class SettingsStore(context: Context) {
|
||||||
|
private val prefs =
|
||||||
|
context.applicationContext.getSharedPreferences("punktfunk_settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun load(): Settings = Settings(
|
||||||
|
width = prefs.getInt(K_W, 0),
|
||||||
|
height = prefs.getInt(K_H, 0),
|
||||||
|
hz = prefs.getInt(K_HZ, 0),
|
||||||
|
bitrateKbps = prefs.getInt(K_BITRATE, 0),
|
||||||
|
compositor = prefs.getInt(K_COMPOSITOR, 0),
|
||||||
|
gamepad = prefs.getInt(K_GAMEPAD, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun save(s: Settings) {
|
||||||
|
prefs.edit()
|
||||||
|
.putInt(K_W, s.width)
|
||||||
|
.putInt(K_H, s.height)
|
||||||
|
.putInt(K_HZ, s.hz)
|
||||||
|
.putInt(K_BITRATE, s.bitrateKbps)
|
||||||
|
.putInt(K_COMPOSITOR, s.compositor)
|
||||||
|
.putInt(K_GAMEPAD, s.gamepad)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val K_W = "width"
|
||||||
|
const val K_H = "height"
|
||||||
|
const val K_HZ = "hz"
|
||||||
|
const val K_BITRATE = "bitrate_kbps"
|
||||||
|
const val K_COMPOSITOR = "compositor"
|
||||||
|
const val K_GAMEPAD = "gamepad"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device's native display mode as a landscape `(width, height, hz)` — the long edge is the
|
||||||
|
* width, since we stream a desktop. Falls back to 1920×1080@60 if the display can't be read.
|
||||||
|
* [context] must be a visual (Activity) context.
|
||||||
|
*/
|
||||||
|
fun nativeDisplayMode(context: Context): Triple<Int, Int, Int> {
|
||||||
|
// getDisplay() throws on a non-visual context rather than returning null — guard it.
|
||||||
|
val display = runCatching { context.display }.getOrNull() ?: return Triple(1920, 1080, 60)
|
||||||
|
val mode = display.mode
|
||||||
|
val w = mode.physicalWidth
|
||||||
|
val h = mode.physicalHeight
|
||||||
|
val hz = mode.refreshRate.toInt().coerceAtLeast(1)
|
||||||
|
return Triple(maxOf(w, h), minOf(w, h), hz)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve [Settings] (with its 0=native placeholders) to the concrete mode to request. */
|
||||||
|
fun Settings.effectiveMode(context: Context): Triple<Int, Int, Int> {
|
||||||
|
val native = nativeDisplayMode(context)
|
||||||
|
val w = if (width > 0) width else native.first
|
||||||
|
val h = if (height > 0) height else native.second
|
||||||
|
val hz = if (hz > 0) hz else native.third
|
||||||
|
return Triple(w, h, hz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UI option tables (value, label). The first entry is always the "auto/native" default. ----
|
||||||
|
|
||||||
|
/** (width, height, label). `(0,0)` = native display. */
|
||||||
|
val RESOLUTION_OPTIONS = listOf(
|
||||||
|
Triple(0, 0, "Native display"),
|
||||||
|
Triple(1280, 720, "1280 × 720"),
|
||||||
|
Triple(1920, 1080, "1920 × 1080"),
|
||||||
|
Triple(2560, 1440, "2560 × 1440"),
|
||||||
|
Triple(3840, 2160, "3840 × 2160"),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** (hz, label). `0` = native refresh. */
|
||||||
|
val REFRESH_OPTIONS = listOf(
|
||||||
|
0 to "Native",
|
||||||
|
30 to "30 Hz",
|
||||||
|
60 to "60 Hz",
|
||||||
|
90 to "90 Hz",
|
||||||
|
120 to "120 Hz",
|
||||||
|
144 to "144 Hz",
|
||||||
|
165 to "165 Hz",
|
||||||
|
240 to "240 Hz",
|
||||||
|
)
|
||||||
|
|
||||||
|
/** (kbps, label). `0` = host default. */
|
||||||
|
val BITRATE_OPTIONS = listOf(
|
||||||
|
0 to "Automatic",
|
||||||
|
10_000 to "10 Mbps",
|
||||||
|
20_000 to "20 Mbps",
|
||||||
|
50_000 to "50 Mbps",
|
||||||
|
100_000 to "100 Mbps",
|
||||||
|
)
|
||||||
|
|
||||||
|
/** index = CompositorPref wire byte. */
|
||||||
|
val COMPOSITOR_OPTIONS = listOf(
|
||||||
|
"Automatic",
|
||||||
|
"KWin (KDE Plasma)",
|
||||||
|
"wlroots (Sway / Hyprland)",
|
||||||
|
"Mutter (GNOME)",
|
||||||
|
"gamescope",
|
||||||
|
)
|
||||||
|
|
||||||
|
/** index = GamepadPref wire byte. */
|
||||||
|
val GAMEPAD_OPTIONS = listOf(
|
||||||
|
"Automatic",
|
||||||
|
"Xbox 360",
|
||||||
|
"DualSense",
|
||||||
|
)
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package io.unom.punktfunk
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream settings. Edits are persisted immediately via [onChange]; [onBack] returns to the connect
|
||||||
|
* screen. Resolution/refresh "Native" resolve from the device display at connect time.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(initial: Settings, onChange: (Settings) -> Unit, onBack: () -> Unit) {
|
||||||
|
var s by remember { mutableStateOf(initial) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
fun update(next: Settings) {
|
||||||
|
s = next
|
||||||
|
onChange(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = onBack)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
|
||||||
|
val (nw, nh, nhz) = nativeDisplayMode(context)
|
||||||
|
SettingDropdown(
|
||||||
|
label = "Resolution",
|
||||||
|
options = RESOLUTION_OPTIONS.map { (w, h, lbl) ->
|
||||||
|
(w to h) to (if (w == 0) "$lbl ($nw × $nh)" else lbl)
|
||||||
|
},
|
||||||
|
selected = s.width to s.height,
|
||||||
|
) { (w, h) -> update(s.copy(width = w, height = h)) }
|
||||||
|
|
||||||
|
SettingDropdown(
|
||||||
|
label = "Refresh rate",
|
||||||
|
options = REFRESH_OPTIONS.map { (hz, lbl) -> hz to (if (hz == 0) "$lbl (${nhz} Hz)" else lbl) },
|
||||||
|
selected = s.hz,
|
||||||
|
) { hz -> update(s.copy(hz = hz)) }
|
||||||
|
|
||||||
|
SettingDropdown(
|
||||||
|
label = "Bitrate",
|
||||||
|
options = BITRATE_OPTIONS,
|
||||||
|
selected = s.bitrateKbps,
|
||||||
|
) { kbps -> update(s.copy(bitrateKbps = kbps)) }
|
||||||
|
|
||||||
|
SettingDropdown(
|
||||||
|
label = "Compositor (virtual-display host backend)",
|
||||||
|
options = COMPOSITOR_OPTIONS.mapIndexed { i, lbl -> i to lbl },
|
||||||
|
selected = s.compositor,
|
||||||
|
) { c -> update(s.copy(compositor = c)) }
|
||||||
|
|
||||||
|
SettingDropdown(
|
||||||
|
label = "Controller type",
|
||||||
|
options = GAMEPAD_OPTIONS.mapIndexed { i, lbl -> i to lbl },
|
||||||
|
selected = s.gamepad,
|
||||||
|
) { g -> update(s.copy(gamepad = g)) }
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
TextButton(onClick = onBack) { Text("Done") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A labelled read-only dropdown over [options] (value → label); calls [onSelect] on a pick. */
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun <T> SettingDropdown(
|
||||||
|
label: String,
|
||||||
|
options: List<Pair<T, String>>,
|
||||||
|
selected: T,
|
||||||
|
onSelect: (T) -> Unit,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val selectedLabel = options.firstOrNull { it.first == selected }?.second
|
||||||
|
?: options.firstOrNull()?.second.orEmpty()
|
||||||
|
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedLabel,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(label) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
options.forEach { (value, lbl) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(lbl) },
|
||||||
|
onClick = {
|
||||||
|
onSelect(value)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,10 @@ object NativeBridge {
|
|||||||
/**
|
/**
|
||||||
* Connect, presenting [certPem]/[keyPem] (both empty = anonymous) and pinning [pinHex] (empty =
|
* Connect, presenting [certPem]/[keyPem] (both empty = anonymous) and pinning [pinHex] (empty =
|
||||||
* trust-on-first-use — read [nativeHostFingerprint] after; else 64-hex host SHA-256, mismatch →
|
* trust-on-first-use — read [nativeHostFingerprint] after; else 64-hex host SHA-256, mismatch →
|
||||||
* `0`). Returns an opaque session handle, or `0` on failure. Pair with exactly one [nativeClose].
|
* `0`). [width]/[height]/[refreshHz] are the requested virtual-output mode (the host streams at
|
||||||
|
* exactly this); [bitrateKbps] 0 = host default; [compositorPref]/[gamepadPref] are the
|
||||||
|
* `CompositorPref`/`GamepadPref` wire bytes (0 = Auto). Returns an opaque session handle, or `0`
|
||||||
|
* on failure. Pair with exactly one [nativeClose].
|
||||||
*/
|
*/
|
||||||
external fun nativeConnect(
|
external fun nativeConnect(
|
||||||
host: String,
|
host: String,
|
||||||
@@ -38,6 +41,9 @@ object NativeBridge {
|
|||||||
certPem: String,
|
certPem: String,
|
||||||
keyPem: String,
|
keyPem: String,
|
||||||
pinHex: String,
|
pinHex: String,
|
||||||
|
bitrateKbps: Int,
|
||||||
|
compositorPref: Int,
|
||||||
|
gamepadPref: Int,
|
||||||
): Long
|
): Long
|
||||||
|
|
||||||
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
|
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
|
||||||
|
|||||||
@@ -109,9 +109,11 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeGenerateIde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex): Long`. `certPem`/
|
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex, bitrateKbps,
|
||||||
/// `keyPem` empty = anonymous, else presented as the persistent identity. `pinHex` empty = TOFU
|
/// compositorPref, gamepadPref): Long`. `certPem`/`keyPem` empty = anonymous, else presented as the
|
||||||
/// (read `nativeHostFingerprint` after), else 64-hex SHA-256 to pin the host (mismatch → 0).
|
/// persistent identity. `pinHex` empty = TOFU (read `nativeHostFingerprint` after), else 64-hex
|
||||||
|
/// SHA-256 to pin the host (mismatch → 0). `bitrateKbps` 0 = host default. `compositorPref`/
|
||||||
|
/// `gamepadPref` are `CompositorPref`/`GamepadPref` wire bytes (0 = Auto; unknown → Auto).
|
||||||
/// Returns an opaque handle, or 0 on failure (logged).
|
/// Returns an opaque handle, or 0 on failure (logged).
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -126,6 +128,9 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
cert_pem: JString<'local>,
|
cert_pem: JString<'local>,
|
||||||
key_pem: JString<'local>,
|
key_pem: JString<'local>,
|
||||||
pin_hex: JString<'local>,
|
pin_hex: JString<'local>,
|
||||||
|
bitrate_kbps: jint,
|
||||||
|
compositor_pref: jint,
|
||||||
|
gamepad_pref: jint,
|
||||||
) -> jlong {
|
) -> jlong {
|
||||||
let host: String = match env.get_string(&host) {
|
let host: String = match env.get_string(&host) {
|
||||||
Ok(s) => s.into(),
|
Ok(s) => s.into(),
|
||||||
@@ -163,12 +168,12 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
&host,
|
&host,
|
||||||
port as u16,
|
port as u16,
|
||||||
mode,
|
mode,
|
||||||
CompositorPref::Auto,
|
CompositorPref::from_u8(compositor_pref.clamp(0, u8::MAX as jint) as u8),
|
||||||
GamepadPref::Auto,
|
GamepadPref::from_u8(gamepad_pref.clamp(0, u8::MAX as jint) as u8),
|
||||||
0, // bitrate_kbps: host default
|
bitrate_kbps.max(0) as u32, // 0 = host default
|
||||||
None, // launch: default app
|
None, // launch: default app
|
||||||
pin, // Some → Crypto on host-fp mismatch
|
pin, // Some → Crypto on host-fp mismatch
|
||||||
identity, // owned (cert, key) PEM, or None (anonymous)
|
identity, // owned (cert, key) PEM, or None (anonymous)
|
||||||
Duration::from_secs(10),
|
Duration::from_secs(10),
|
||||||
) {
|
) {
|
||||||
Ok(client) => {
|
Ok(client) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user