Работающие звонки
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <android/log.h>
|
||||
@@ -125,6 +126,12 @@ struct AdditionalTsState {
|
||||
uint64_t base_timestamp = 0;
|
||||
};
|
||||
|
||||
struct SenderTsOffsetState {
|
||||
bool initialized = false;
|
||||
bool enabled = false;
|
||||
uint64_t offset = 0;
|
||||
};
|
||||
|
||||
static inline uint16_t load16_be(const uint8_t* p) {
|
||||
return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
|
||||
}
|
||||
@@ -334,6 +341,25 @@ static inline void fill_nonce_from_ts32(uint32_t ts, uint8_t nonce[24]) {
|
||||
nonce[7] = (uint8_t)(ts);
|
||||
}
|
||||
|
||||
static inline void fill_nonce_from_ts64(uint64_t ts, uint8_t nonce[24]) {
|
||||
nonce[0] = (uint8_t)(ts >> 56);
|
||||
nonce[1] = (uint8_t)(ts >> 48);
|
||||
nonce[2] = (uint8_t)(ts >> 40);
|
||||
nonce[3] = (uint8_t)(ts >> 32);
|
||||
nonce[4] = (uint8_t)(ts >> 24);
|
||||
nonce[5] = (uint8_t)(ts >> 16);
|
||||
nonce[6] = (uint8_t)(ts >> 8);
|
||||
nonce[7] = (uint8_t)(ts);
|
||||
}
|
||||
|
||||
static inline uint64_t monotonic_48k_ticks() {
|
||||
struct timespec ts {};
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
|
||||
const uint64_t sec = (uint64_t)ts.tv_sec;
|
||||
const uint64_t nsec = (uint64_t)ts.tv_nsec;
|
||||
return sec * 48000ULL + (nsec * 48000ULL) / 1000000000ULL;
|
||||
}
|
||||
|
||||
static inline uint32_t opus_base_frame_samples(uint8_t config) {
|
||||
// RFC 6716 TOC config mapping at 48 kHz.
|
||||
if (config <= 11) {
|
||||
@@ -550,6 +576,7 @@ public:
|
||||
bool nonce_from_generated_ts = false;
|
||||
bool nonce_from_additional_data = false;
|
||||
bool additional_was_rtp_header = false;
|
||||
bool additional_used_mono_offset = false;
|
||||
uint32_t generated_ts_used = 0;
|
||||
|
||||
// Build nonce from RTP timestamp in additional_data (preferred).
|
||||
@@ -579,23 +606,36 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
if (nonce_from_rtp_header && header_size <= frame.size()) {
|
||||
// Keep RTP header clear, encrypt payload only.
|
||||
if (header_size > 0) {
|
||||
memcpy(encrypted_frame.data(), frame.data(), header_size);
|
||||
// Some Android sender pipelines expose stream-relative ad8 timestamps
|
||||
// (0, 960, 1920, ...), while desktop receiver expects an absolute base.
|
||||
// For interop, add a monotonic 48k offset once when first ad8 is tiny.
|
||||
if (nonce_from_additional_data &&
|
||||
additional_data.size() == 8 &&
|
||||
!additional_was_rtp_header &&
|
||||
additional_data.data() != nullptr) {
|
||||
const uint64_t ad_ts64 = load64_be(additional_data.data());
|
||||
if (!sender_ts_offset_.initialized) {
|
||||
sender_ts_offset_.initialized = true;
|
||||
// Keep pure raw-abs mode by default; desktop is the source of truth.
|
||||
sender_ts_offset_.enabled = false;
|
||||
sender_ts_offset_.offset = 0ULL;
|
||||
diag_event("ENC ad8-base init ssrc=%u ad_ts=%llu use_mono=%d mono_off=%llu\n",
|
||||
ssrc,
|
||||
(unsigned long long)ad_ts64,
|
||||
sender_ts_offset_.enabled ? 1 : 0,
|
||||
(unsigned long long)sender_ts_offset_.offset);
|
||||
}
|
||||
if (sender_ts_offset_.enabled) {
|
||||
const uint64_t ts_adj = ad_ts64 + sender_ts_offset_.offset;
|
||||
fill_nonce_from_ts64(ts_adj, nonce);
|
||||
additional_used_mono_offset = true;
|
||||
}
|
||||
const size_t payload_size = frame.size() - header_size;
|
||||
rosetta_xchacha20_xor(
|
||||
encrypted_frame.data() + header_size,
|
||||
frame.data() + header_size,
|
||||
payload_size,
|
||||
nonce,
|
||||
key_);
|
||||
} else {
|
||||
// Legacy path: frame is payload-only.
|
||||
rosetta_xchacha20_xor(encrypted_frame.data(),
|
||||
frame.data(), frame.size(), nonce, key_);
|
||||
}
|
||||
|
||||
// Desktop createEncodedStreams encrypts full encoded chunk.
|
||||
// To stay wire-compatible, do not preserve any leading RTP-like bytes.
|
||||
rosetta_xchacha20_xor(encrypted_frame.data(),
|
||||
frame.data(), frame.size(), nonce, key_);
|
||||
*bytes_written = frame.size();
|
||||
|
||||
if (nonce_from_generated_ts) {
|
||||
@@ -628,7 +668,9 @@ public:
|
||||
: (nonce_from_additional_data
|
||||
? (additional_was_rtp_header
|
||||
? "ad-rtp"
|
||||
: (additional_used_relative_ts ? "raw-rel" : "raw-abs"))
|
||||
: (additional_used_mono_offset
|
||||
? "raw-abs+mono"
|
||||
: (additional_used_relative_ts ? "raw-rel" : "raw-abs")))
|
||||
: "raw-abs"));
|
||||
LOGI("ENC frame#%d mt=%s ssrc=%u sz=%zu ad=%zu hdr=%zu mode=%s nonce_ts=%u gen_ts=%u next_step=%u rtp_ok=%d rtp_seq=%u rtp_ts=%u rtp_ssrc=%u opus_ok=%d key_fp=%08x in_h=%08x out_h=%08x ad8=%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
n, media_type_name(media_type), ssrc, frame.size(), additional_data.size(), header_size, mode,
|
||||
@@ -663,6 +705,7 @@ private:
|
||||
mutable std::atomic<int> diag_count_{0};
|
||||
mutable RtpProbeState rtp_probe_;
|
||||
mutable GeneratedTsState generated_ts_;
|
||||
mutable SenderTsOffsetState sender_ts_offset_;
|
||||
uint32_t key_fingerprint_ = 0;
|
||||
uint8_t key_[32];
|
||||
};
|
||||
@@ -746,20 +789,8 @@ public:
|
||||
|
||||
bool used_generated_resync = false;
|
||||
|
||||
if (nonce_from_rtp_header && header_size <= encrypted_frame.size()) {
|
||||
if (header_size > 0) {
|
||||
memcpy(frame.data(), encrypted_frame.data(), header_size);
|
||||
}
|
||||
const size_t payload_size = encrypted_frame.size() - header_size;
|
||||
rosetta_xchacha20_xor(
|
||||
frame.data() + header_size,
|
||||
encrypted_frame.data() + header_size,
|
||||
payload_size,
|
||||
nonce,
|
||||
key_);
|
||||
} else {
|
||||
rosetta_xchacha20_xor(frame.data(), encrypted_frame.data(), encrypted_frame.size(), nonce, key_);
|
||||
}
|
||||
// Desktop createEncodedStreams decrypts full encoded chunk.
|
||||
rosetta_xchacha20_xor(frame.data(), encrypted_frame.data(), encrypted_frame.size(), nonce, key_);
|
||||
|
||||
if (nonce_from_additional_data) {
|
||||
bool plausible = is_plausible_decrypted_audio_frame(frame.data(), encrypted_frame.size());
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.util.Log
|
||||
import com.rosetta.messenger.data.MessageRepository
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.IdentityHashMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -137,8 +136,8 @@ object CallManager {
|
||||
|
||||
// E2EE (XChaCha20 — compatible with Desktop)
|
||||
private var sharedKeyBytes: ByteArray? = null
|
||||
private val senderEncryptors = IdentityHashMap<RtpSender, XChaCha20E2EE.Encryptor>()
|
||||
private val receiverDecryptors = IdentityHashMap<RtpReceiver, XChaCha20E2EE.Decryptor>()
|
||||
private val senderEncryptors = LinkedHashMap<String, XChaCha20E2EE.Encryptor>()
|
||||
private val receiverDecryptors = LinkedHashMap<String, XChaCha20E2EE.Decryptor>()
|
||||
private var pendingAudioSenderForE2ee: RtpSender? = null
|
||||
private var lastRemoteOfferFingerprint: String = ""
|
||||
private var lastLocalOfferFingerprint: String = ""
|
||||
@@ -1063,11 +1062,22 @@ object CallManager {
|
||||
breadcrumb("STATE[$marker] ${buildStateSnapshot()}")
|
||||
}
|
||||
|
||||
private fun senderMapKey(sender: RtpSender): String {
|
||||
val id = runCatching { sender.id() }.getOrNull().orEmpty()
|
||||
return if (id.isNotBlank()) "sid:$id" else "sender@${System.identityHashCode(sender)}"
|
||||
}
|
||||
|
||||
private fun receiverMapKey(receiver: RtpReceiver): String {
|
||||
val id = runCatching { receiver.id() }.getOrNull().orEmpty()
|
||||
return if (id.isNotBlank()) "rid:$id" else "recv@${System.identityHashCode(receiver)}"
|
||||
}
|
||||
|
||||
private fun attachSenderE2EE(sender: RtpSender?) {
|
||||
if (!e2eeAvailable) return
|
||||
val key = sharedKeyBytes ?: return
|
||||
if (sender == null) return
|
||||
val existing = senderEncryptors[sender]
|
||||
val mapKey = senderMapKey(sender)
|
||||
val existing = senderEncryptors[mapKey]
|
||||
if (existing != null) {
|
||||
runCatching { sender.setFrameEncryptor(existing) }
|
||||
return
|
||||
@@ -1086,7 +1096,7 @@ object CallManager {
|
||||
breadcrumb("4. calling sender.setFrameEncryptor…")
|
||||
sender.setFrameEncryptor(enc)
|
||||
breadcrumb("5. setFrameEncryptor OK!")
|
||||
senderEncryptors[sender] = enc
|
||||
senderEncryptors[mapKey] = enc
|
||||
pendingAudioSenderForE2ee = null
|
||||
} catch (e: Throwable) {
|
||||
saveCrashReport("attachSenderE2EE failed", e)
|
||||
@@ -1104,7 +1114,8 @@ object CallManager {
|
||||
if (!e2eeAvailable) return
|
||||
val key = sharedKeyBytes ?: return
|
||||
if (receiver == null) return
|
||||
val existing = receiverDecryptors[receiver]
|
||||
val mapKey = receiverMapKey(receiver)
|
||||
val existing = receiverDecryptors[mapKey]
|
||||
if (existing != null) {
|
||||
runCatching { receiver.setFrameDecryptor(existing) }
|
||||
return
|
||||
@@ -1123,7 +1134,7 @@ object CallManager {
|
||||
breadcrumb("9. calling receiver.setFrameDecryptor…")
|
||||
receiver.setFrameDecryptor(dec)
|
||||
breadcrumb("10. setFrameDecryptor OK!")
|
||||
receiverDecryptors[receiver] = dec
|
||||
receiverDecryptors[mapKey] = dec
|
||||
} catch (e: Throwable) {
|
||||
saveCrashReport("attachReceiverE2EE failed", e)
|
||||
Log.e(TAG, "E2EE: receiver decryptor failed", e)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.rosetta.messenger.ui.crashlogs
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -8,13 +9,16 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -263,6 +267,8 @@ private fun CrashDetailScreen(
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -274,6 +280,14 @@ private fun CrashDetailScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(crashReport.content))
|
||||
Toast.makeText(context, "Full log copied", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.ContentCopy, contentDescription = "Copy Full Log")
|
||||
}
|
||||
IconButton(onClick = { /* TODO: Share */ }) {
|
||||
Icon(Icons.Default.Share, contentDescription = "Share")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user