- Settings UI: radio buttons for encode codec selection - Persisted via SettingsRepository - Passed through WzpEngine.startCall(profile=) → JNI → Rust CallStartConfig - Decode always accepts all codecs (per-packet codec_id switch) - 0 = Opus 24k (GOOD), 1 = Opus 6k (DEGRADED), 2 = Codec2 1.2k (CATASTROPHIC) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
185 lines
6.6 KiB
Kotlin
185 lines
6.6 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
|
|
*/
|
|
/**
|
|
* @param profile 0 = Opus GOOD, 1 = Opus DEGRADED, 2 = Codec2 CATASTROPHIC
|
|
*/
|
|
fun startCall(relayAddr: String, room: String, seedHex: String = "", token: String = "", alias: String = "", profile: Int = 0): Int {
|
|
check(nativeHandle != 0L) { "Engine not initialized" }
|
|
val result = nativeStartCall(nativeHandle, relayAddr, room, seedHex, token, alias, profile)
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* Write captured PCM from a DirectByteBuffer — zero JNI array copy.
|
|
* The buffer must be a direct ByteBuffer with native byte order containing i16 samples.
|
|
* Called from the AudioRecord capture thread.
|
|
*/
|
|
fun writeAudioDirect(buffer: java.nio.ByteBuffer, sampleCount: Int): Int {
|
|
if (nativeHandle == 0L) return 0
|
|
return nativeWriteAudioDirect(nativeHandle, buffer, sampleCount)
|
|
}
|
|
|
|
/**
|
|
* Read decoded PCM into a DirectByteBuffer — zero JNI array copy.
|
|
* The buffer must be a direct ByteBuffer with native byte order.
|
|
* Called from the AudioTrack playout thread.
|
|
*/
|
|
fun readAudioDirect(buffer: java.nio.ByteBuffer, maxSamples: Int): Int {
|
|
if (nativeHandle == 0L) return 0
|
|
return nativeReadAudioDirect(nativeHandle, buffer, maxSamples)
|
|
}
|
|
|
|
// -- JNI native methods --------------------------------------------------
|
|
|
|
private external fun nativeInit(): Long
|
|
private external fun nativeStartCall(
|
|
handle: Long, relay: String, room: String, seed: String, token: String, alias: String, profile: Int
|
|
): 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 nativeWriteAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, sampleCount: Int): Int
|
|
private external fun nativeReadAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, maxSamples: Int): Int
|
|
private external fun nativeDestroy(handle: Long)
|
|
private external fun nativePingRelay(handle: Long, relay: String): String?
|
|
|
|
/**
|
|
* Ping a relay server. Requires engine to be initialized.
|
|
* Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` or null.
|
|
*/
|
|
fun pingRelay(address: String): String? {
|
|
if (nativeHandle == 0L) return null
|
|
return nativePingRelay(nativeHandle, address)
|
|
}
|
|
|
|
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
|
|
}
|