Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m34s
- Add SettingsScreen with identity (alias, key backup/restore), audio defaults, server management, network prefs, and default room - SettingsRepository persists all settings via SharedPreferences - Auto-generate random display names on first launch (e.g. "Swift Wolf") - Thread alias through CallOffer → relay handshake → RoomUpdate broadcast - Derive caller fingerprint from identity key in relay handshake (fixes null fingerprints when --auth-url is not set) - Persist identity seed for stable fingerprints across reconnects - Add alias field to SignalMessage::CallOffer (serde default for backward compat) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
5.0 KiB
Kotlin
150 lines
5.0 KiB
Kotlin
package com.wzp.engine
|
|
|
|
/**
|
|
* Native VoIP engine wrapper. Delegates all work to libwzp_android.so via JNI.
|
|
*
|
|
* Lifecycle:
|
|
* 1. Construct with a [WzpCallback]
|
|
* 2. Call [init] to create the native engine
|
|
* 3. Call [startCall] to begin a VoIP session
|
|
* 4. Use [setMute], [setSpeaker], [getStats], [forceProfile] during the call
|
|
* 5. Call [stopCall] to end the session
|
|
* 6. Call [destroy] when the engine is no longer needed
|
|
*
|
|
* Thread safety: all methods must be called from the same thread (typically main).
|
|
*/
|
|
class WzpEngine(private val callback: WzpCallback) {
|
|
|
|
/** Opaque pointer to the native EngineHandle. 0 means not initialised. */
|
|
private var nativeHandle: Long = 0L
|
|
|
|
/** Whether the engine has been initialised. */
|
|
val isInitialized: Boolean get() = nativeHandle != 0L
|
|
|
|
/** Create the native engine. Must be called before any other method. */
|
|
fun init() {
|
|
check(nativeHandle == 0L) { "Engine already initialized" }
|
|
nativeHandle = nativeInit()
|
|
check(nativeHandle != 0L) { "Native engine creation failed" }
|
|
}
|
|
|
|
/**
|
|
* Start a call.
|
|
*
|
|
* @param relayAddr relay server address (host:port)
|
|
* @param room room identifier (used as QUIC SNI)
|
|
* @param seedHex 64-char hex-encoded 32-byte identity seed (empty = random)
|
|
* @param token authentication token (empty = no auth)
|
|
* @param alias display name sent to relay for room participant list
|
|
* @return 0 on success, negative error code on failure
|
|
*/
|
|
fun startCall(relayAddr: String, room: String, seedHex: String = "", token: String = "", alias: String = ""): Int {
|
|
check(nativeHandle != 0L) { "Engine not initialized" }
|
|
val result = nativeStartCall(nativeHandle, relayAddr, room, seedHex, token, alias)
|
|
if (result == 0) {
|
|
callback.onCallStateChanged(CallStateConstants.CONNECTING)
|
|
} else {
|
|
callback.onError(result, "Failed to start call")
|
|
}
|
|
return result
|
|
}
|
|
|
|
/** Stop the active call. Safe to call when no call is active. */
|
|
fun stopCall() {
|
|
if (nativeHandle != 0L) {
|
|
nativeStopCall(nativeHandle)
|
|
callback.onCallStateChanged(CallStateConstants.CLOSED)
|
|
}
|
|
}
|
|
|
|
/** Mute or unmute the microphone. */
|
|
fun setMute(muted: Boolean) {
|
|
if (nativeHandle != 0L) nativeSetMute(nativeHandle, muted)
|
|
}
|
|
|
|
/** Enable or disable loudspeaker mode. */
|
|
fun setSpeaker(speaker: Boolean) {
|
|
if (nativeHandle != 0L) nativeSetSpeaker(nativeHandle, speaker)
|
|
}
|
|
|
|
|
|
/**
|
|
* Get current call statistics as a JSON string.
|
|
*
|
|
* @return JSON-serialised [CallStats], or `"{}"` if the engine is not initialised.
|
|
*/
|
|
fun getStats(): String {
|
|
if (nativeHandle == 0L) return "{}"
|
|
return try {
|
|
nativeGetStats(nativeHandle) ?: "{}"
|
|
} catch (_: Exception) {
|
|
"{}"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force a quality profile, overriding adaptive selection.
|
|
*
|
|
* @param profile 0 = GOOD, 1 = DEGRADED, 2 = CATASTROPHIC
|
|
*/
|
|
fun forceProfile(profile: Int) {
|
|
if (nativeHandle != 0L) nativeForceProfile(nativeHandle, profile)
|
|
}
|
|
|
|
/** Destroy the native engine and free all resources. The instance must not be reused. */
|
|
fun destroy() {
|
|
if (nativeHandle != 0L) {
|
|
nativeDestroy(nativeHandle)
|
|
nativeHandle = 0L
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write captured PCM samples into the engine's capture ring buffer.
|
|
* Called from the AudioRecord capture thread.
|
|
*/
|
|
fun writeAudio(pcm: ShortArray): Int {
|
|
if (nativeHandle == 0L) return 0
|
|
return nativeWriteAudio(nativeHandle, pcm)
|
|
}
|
|
|
|
/**
|
|
* Read decoded PCM samples from the engine's playout ring buffer.
|
|
* Called from the AudioTrack playout thread.
|
|
*/
|
|
fun readAudio(pcm: ShortArray): Int {
|
|
if (nativeHandle == 0L) return 0
|
|
return nativeReadAudio(nativeHandle, pcm)
|
|
}
|
|
|
|
// -- JNI native methods --------------------------------------------------
|
|
|
|
private external fun nativeInit(): Long
|
|
private external fun nativeStartCall(
|
|
handle: Long, relay: String, room: String, seed: String, token: String, alias: String
|
|
): Int
|
|
private external fun nativeStopCall(handle: Long)
|
|
private external fun nativeSetMute(handle: Long, muted: Boolean)
|
|
private external fun nativeSetSpeaker(handle: Long, speaker: Boolean)
|
|
private external fun nativeGetStats(handle: Long): String?
|
|
private external fun nativeForceProfile(handle: Long, profile: Int)
|
|
private external fun nativeWriteAudio(handle: Long, pcm: ShortArray): Int
|
|
private external fun nativeReadAudio(handle: Long, pcm: ShortArray): Int
|
|
private external fun nativeDestroy(handle: Long)
|
|
|
|
companion object {
|
|
init {
|
|
System.loadLibrary("wzp_android")
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Integer constants matching the Rust [CallState] enum ordinals. */
|
|
object CallStateConstants {
|
|
const val IDLE = 0
|
|
const val CONNECTING = 1
|
|
const val ACTIVE = 2
|
|
const val RECONNECTING = 3
|
|
const val CLOSED = 4
|
|
}
|