diff --git a/tools/chatgui-android/video-tests/build.gradle.kts b/tools/chatgui-android/video-tests/build.gradle.kts
index a1206423..067ea13c 100644
--- a/tools/chatgui-android/video-tests/build.gradle.kts
+++ b/tools/chatgui-android/video-tests/build.gradle.kts
@@ -14,7 +14,8 @@ kotlin {
"com/utun/chat/data/CallVideoCadence.kt", "com/utun/chat/data/CallVideoOrientation.kt")
kotlin.include("com/utun/chat/data/StreamHost.kt", "com/utun/chat/data/StreamPlayerEngine.kt",
"com/utun/chat/data/StreamAudioMixer.kt", "com/utun/chat/data/StreamAudioOutput.kt",
- "com/utun/chat/data/StreamAudioMix.kt", "com/utun/chat/data/StreamReadQueue.kt", "com/utun/chat/data/StreamPlaybackTelemetry.kt")
+ "com/utun/chat/data/StreamAudioMix.kt", "com/utun/chat/data/StreamReadQueue.kt", "com/utun/chat/data/StreamPlaybackTelemetry.kt",
+ "com/utun/chat/data/StreamCaptureEngine.kt", "com/utun/chat/data/StreamCaptureConfig.kt")
}
}
dependencies {
diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/StreamHost.kt b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/StreamHost.kt
index 9ac85446..1aa52b02 100644
--- a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/StreamHost.kt
+++ b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/StreamHost.kt
@@ -5,6 +5,27 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.flow.MutableStateFlow
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+
+typealias RadioCompressorSettings = Any
+data class StreamCaptureMode(val camera: String = "0", val width: Int = 640, val height: Int = 480, val fps: Int = 30,
+ val minimumFps: Int = 30, val highSpeed: Boolean = false, val codec: String = "host.encoder", val bitrate: Int = 1_000_000,
+ val videoCodec: StreamVideoCodec = StreamVideoCodec.H264, val minimumBitrate: Int = 1, val maximumBitrate: Int = 12_000_000,
+ val encoderProfile: Int? = 1) {
+ val label get() = "${width}x$height@$fps"
+ val manualBitrateRange get() = minimumBitrate..maximumBitrate
+}
+object MicrophoneCapture {
+ data class Frame(val pcm: ShortArray, val timeNs: Long, val sequence: Long)
+ class Subscription : AutoCloseable {
+ val queue = LinkedBlockingQueue()
+ var failures = emptySet()
+ var closed = false
+ fun poll(): Any? = queue.poll(20, TimeUnit.MILLISECONDS)
+ override fun close() { check("close" !in failures); closed = true }
+ }
+}
object CallController { var blocked = false; fun blocksMedia() = blocked }
object StreamController { private val ids = AtomicLong(100); fun nextId() = ids.incrementAndGet() }
@@ -13,6 +34,11 @@ object MediaAudioController {
val state = MutableStateFlow(State())
var failures = emptySet()
val outputs = ConcurrentHashMap.newKeySet()
+ val subscriptions = ConcurrentLinkedQueue()
+ fun subscribe(name: String): MicrophoneCapture.Subscription {
+ check("subscribe" !in failures)
+ return MicrophoneCapture.Subscription().also { subscriptions.add(it) }
+ }
fun attachOutput(track: AudioTrack): AutoCloseable {
check("attach" !in failures); outputs.add(track)
return AutoCloseable { check("detach" !in failures); outputs.remove(track) }
@@ -24,6 +50,7 @@ object HostStreams {
val ports = ConcurrentHashMap.newKeySet()
val signals = ConcurrentHashMap.newKeySet()
val playbacks = ConcurrentHashMap.newKeySet()
+ val opuses = ConcurrentHashMap.newKeySet()
val records = ConcurrentLinkedQueue()
private val sequence = AtomicLong()
@Volatile var failures = emptySet()
@@ -40,8 +67,10 @@ object HostStreams {
fun waitSignal(handle: Long): Int { onWait(); access("signal wait"); check(handle in signals); Thread.sleep(2); return 0 }
fun createPlayback(): Long { access("playback create"); return sequence.incrementAndGet().also { playbacks.add(it) } }
fun closePlayback(handle: Long) { access("playback close"); check(playbacks.remove(handle)) }
+ fun createOpus(): Long { access("opus create"); return sequence.incrementAndGet().also { opuses.add(it) } }
+ fun closeOpus(handle: Long) { access("opus close"); check(opuses.remove(handle)) }
fun reset() {
- ports.clear(); signals.clear(); playbacks.clear(); records.clear(); failures = emptySet()
+ ports.clear(); signals.clear(); playbacks.clear(); opuses.clear(); records.clear(); failures = emptySet()
audioEnabled = false; status = false; onWait = {}; onPull = {}
}
}
diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt
index 418d3732..be19f40b 100644
--- a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt
+++ b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt
@@ -66,7 +66,8 @@ object NativeLib {
fun callAudioClock(id: Long, now: Long, out: LongArray): Boolean { out[5] = 1; return true }
fun setChatSetting(name: String, value: String) {}
fun isReady() = true
- fun streamOpen(id: Long, group: Long, source: Long, audio: Boolean, epoch: Long) = HostStreams.open(id)
+ fun streamOpen(id: Long, group: Long, source: Long = 0, width: Int = 0, height: Int = 0, rotation: Int = 0,
+ audio: Boolean = true, videoCodec: Int = 1, epoch: Long = 0) = HostStreams.open(id)
fun streamStop(id: Long) = HostStreams.stop(id)
fun streamSignalCreate(id: Long) = HostStreams.createSignal()
fun streamSignalClose(handle: Long) = HostStreams.closeSignal(handle)
@@ -92,6 +93,14 @@ object NativeLib {
fun streamPlaybackUpdate(handle: Long, now: Long, played: Long, ready: Long, guard: Long, starved: Boolean): LongArray {
HostStreams.access("update"); return longArrayOf(1000 + played * 1_000_000 / 48_000, 0, 0, 1000, 0, 1, 0, 0, 3, 0, 0)
}
+ fun streamSetAudioEnabled(id: Long, enabled: Boolean): Boolean { HostStreams.access("audio enabled"); return true }
+ fun streamFeed(id: Long, video: Boolean, pts: Long, key: Boolean, data: ByteArray, revision: Long = 0, width: Int = 0,
+ height: Int = 0, rotation: Int = 0, audio: Boolean = true, codec: Int = 1): Int { HostStreams.access("feed"); return 1 }
+ fun streamOpusCreate(encode: Boolean) = HostStreams.createOpus()
+ fun streamOpusClose(handle: Long) = HostStreams.closeOpus(handle)
+ fun streamOpusEncode(handle: Long, pcm: ShortArray, compressor: RadioCompressorSettings): ByteArray? {
+ HostStreams.access("opus encode"); check(handle in HostStreams.opuses); return byteArrayOf(1)
+ }
}
/** Native playout/encoded FIFO: только управляемые границы, без загрузки JNI/декодирования/сетевого ядра. */