From 41a4a2e37e542b83f6835ff14c8d4ccaf2606482 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 01:59:17 +0100 Subject: [PATCH] Compile real publisher capture with controlled microphone and native boundaries --- .../video-tests/build.gradle.kts | 3 +- .../kotlin/com/utun/chat/data/StreamHost.kt | 31 ++++++++++++++++++- .../kotlin/com/utun/chat/data/VideoHost.kt | 11 ++++++- 3 files changed, 42 insertions(+), 3 deletions(-) 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/декодирования/сетевого ядра. */