diff --git a/desktop/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/desktop/src-tauri/gen/android/app/src/main/AndroidManifest.xml
index 97a6a67..23d83b2 100644
--- a/desktop/src-tauri/gen/android/app/src/main/AndroidManifest.xml
+++ b/desktop/src-tauri/gen/android/app/src/main/AndroidManifest.xml
@@ -3,7 +3,9 @@
+
+
diff --git a/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt b/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt
index b6a5b9e..a0430f4 100644
--- a/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt
+++ b/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt
@@ -16,10 +16,19 @@ class MainActivity : TauriActivity() {
private const val AUDIO_PERMISSIONS_REQUEST = 4242
private val REQUIRED_AUDIO_PERMISSIONS = arrayOf(
Manifest.permission.RECORD_AUDIO,
- Manifest.permission.MODIFY_AUDIO_SETTINGS
+ Manifest.permission.MODIFY_AUDIO_SETTINGS,
+ Manifest.permission.CAMERA
)
}
+ // NOTE: granting CAMERA at the Android system layer is necessary but NOT
+ // sufficient for video on Android. Tauri/Wry's internal WebChromeClient
+ // does not currently grant `getUserMedia` permission requests, so the
+ // browser-layer getUserMedia call still fails even after the OS grants
+ // CAMERA. Fixing this needs either a Tauri plugin that overrides the
+ // WebChromeClient, or a native Camera2/CameraX capture path that bypasses
+ // the WebView. Tracked as a follow-up.
+
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs
index 5e986bf..d7cbd7e 100644
--- a/desktop/src-tauri/src/engine.rs
+++ b/desktop/src-tauri/src/engine.rs
@@ -615,10 +615,12 @@ impl CallEngine {
video_codec = ?hs.video_codec,
"first-join diag: connected to relay, handshake complete"
);
- Arc::new(wzp_client::encrypted_transport::EncryptingTransport::new(
- transport,
- hs.session,
- ))
+ // NOTE: see comment in CallEngine::start (~line 1585) — we intentionally
+ // do NOT wrap with EncryptingTransport. The pairwise client↔relay session
+ // key can't be used end-to-end without MLS or relay re-encryption.
+ drop(hs.session);
+ let _ = hs.video_codec;
+ transport
} else {
info!(
t_ms = call_t0.elapsed().as_millis(),
@@ -1584,6 +1586,12 @@ impl CallEngine {
// accept_handshake handler. See the android branch's
// comment for the full rationale.
let quinn_transport = transport.clone();
+ // NOTE: EncryptingTransport is intentionally NOT wrapping the transport here.
+ // The client↔relay handshake derives a pairwise session key, but the relay
+ // forwards media without decrypt+re-encrypt — so a recipient with a different
+ // pairwise key cannot decrypt the sender's ciphertext. True E2E for the SFU
+ // model needs MLS group keys (or hop-by-hop relay re-encryption); until that
+ // PRD lands, media goes plaintext-over-QUIC-TLS to the relay.
let (_negotiated_video_codec, transport): (_, Arc) =
if !is_direct_p2p {
let hs =
@@ -1594,13 +1602,8 @@ impl CallEngine {
e
})?;
info!(video_codec = ?hs.video_codec, "handshake complete");
- let enc = Arc::new(
- wzp_client::encrypted_transport::EncryptingTransport::new(
- transport,
- hs.session,
- ),
- );
- (hs.video_codec, enc)
+ drop(hs.session);
+ (hs.video_codec, transport)
} else {
info!("direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)");
(None, transport)