Compile real publisher capture with controlled microphone and native boundaries

This commit is contained in:
evgeny committed 2026-10-29 13:34:25 +01:00
1 parent 7a2fc0112c
commit 41a4a2e37e
3 files changed
+42 -3

No files matched your search

@@ -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 {
@@ -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<Frame>()
var failures = emptySet<String>()
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<String>()
val outputs = ConcurrentHashMap.newKeySet<AudioTrack>()
val subscriptions = ConcurrentLinkedQueue<MicrophoneCapture.Subscription>()
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<Long>()
val signals = ConcurrentHashMap.newKeySet<Long>()
val playbacks = ConcurrentHashMap.newKeySet<Long>()
val opuses = ConcurrentHashMap.newKeySet<Long>()
val records = ConcurrentLinkedQueue<ByteArray>()
private val sequence = AtomicLong()
@Volatile var failures = emptySet<String>()
@@ -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 = {}
}
}
@@ -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/декодирования/сетевого ядра. */