Feed Android stream mixer from prepared PCM without holding DSP lock
This commit is contained in:
1 parent
50cf61e9cd
commit
4fa3b18cc2
4 files changed
+76
-42
No files matched your search
@@ -225,7 +225,10 @@ object NativeLib {
|
||||
external fun streamSignalClose(handle: Long)
|
||||
fun streamPlaybackCreate(id: Long, pts: Long, audio: Boolean): Long = nativeStreamPlaybackCreate(id, pts, audio)
|
||||
fun streamPlaybackFeed(handle: Long, pts: Long, packet: ByteArray): Int = nativeStreamPlaybackFeed(handle, pts, packet)
|
||||
fun streamPlaybackPull(handle: Long, frames: Int, limit: Long): ShortArray? = nativeStreamPlaybackPull(handle, frames, limit)
|
||||
fun streamPlaybackPrepare(handle: Long): Int = nativeStreamPlaybackPrepare(handle)
|
||||
fun streamPlaybackBuffered(handle: Long): Int = nativeStreamPlaybackBuffered(handle)
|
||||
fun streamPlaybackPull(handle: Long, pcm: ShortArray, frames: Int, limit: Long): Int =
|
||||
nativeStreamPlaybackPull(handle, pcm, frames, limit)
|
||||
fun streamPlaybackSilence(handle: Long, frames: Int) { nativeStreamPlaybackSilence(handle, frames) }
|
||||
fun streamPlaybackUpdate(handle: Long, now: Long, played: Long, ready: Long, guard: Long, starved: Boolean): LongArray? =
|
||||
nativeStreamPlaybackUpdate(handle, now, played, ready, guard, starved)
|
||||
@@ -251,7 +254,9 @@ object NativeLib {
|
||||
private external fun nativeStreamResync(id: Long): Boolean
|
||||
private external fun nativeStreamPlaybackCreate(id: Long, pts: Long, audio: Boolean): Long
|
||||
private external fun nativeStreamPlaybackFeed(handle: Long, pts: Long, packet: ByteArray): Int
|
||||
private external fun nativeStreamPlaybackPull(handle: Long, frames: Int, limit: Long): ShortArray?
|
||||
private external fun nativeStreamPlaybackPrepare(handle: Long): Int
|
||||
private external fun nativeStreamPlaybackBuffered(handle: Long): Int
|
||||
private external fun nativeStreamPlaybackPull(handle: Long, pcm: ShortArray, frames: Int, limit: Long): Int
|
||||
private external fun nativeStreamPlaybackSilence(handle: Long, frames: Int)
|
||||
private external fun nativeStreamPlaybackUpdate(handle: Long, now: Long, played: Long, ready: Long,
|
||||
guard: Long, starved: Boolean): LongArray?
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
/** Один AudioTrack для всех трансляций. Worker не удерживает общий lock при обращении к decoder.
|
||||
* Input закрывается после исключения из списка; его собственный lock защищает native DSP от release. */
|
||||
/** Один AudioTrack для всех трансляций. Mixer только копирует и смешивает готовый PCM.
|
||||
* Input закрывается после исключения из списка; его короткий lock защищает consumer от release. */
|
||||
internal object StreamAudioMixer {
|
||||
data class OutputState(val played: Long = 0, val leadUs: Long = 0)
|
||||
@Volatile var output = OutputState()
|
||||
private set
|
||||
private val lock = Any()
|
||||
private val inputs = linkedSetOf<StreamMixedPlayback>()
|
||||
private var snapshot = emptyList<StreamMixedPlayback>()
|
||||
private var worker: Thread? = null
|
||||
|
||||
fun attach(input: StreamMixedPlayback) = synchronized(lock) {
|
||||
check(inputs.add(input))
|
||||
snapshot = inputs.toList()
|
||||
if (worker == null) {
|
||||
output = OutputState()
|
||||
worker = Thread(::run, "stream-mixer").apply { start() }
|
||||
@@ -21,6 +23,7 @@ internal object StreamAudioMixer {
|
||||
|
||||
fun detach(input: StreamMixedPlayback) = synchronized(lock) {
|
||||
inputs.remove(input)
|
||||
snapshot = inputs.toList()
|
||||
LogManager.addLog("INFO", "stream", "mixer input detached id=${input.id} count=${inputs.size}")
|
||||
}
|
||||
|
||||
@@ -39,6 +42,9 @@ internal object StreamAudioMixer {
|
||||
val mix = StreamMixBuffer()
|
||||
var generated = 0L
|
||||
try {
|
||||
try { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO) }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "stream", "mixer audio priority unavailable: ${e.message}") }
|
||||
LogManager.addLog("INFO", "stream", "mixer started: ready PCM only, blockFrames=${mix.frames}")
|
||||
while (true) {
|
||||
val current = synchronized(lock) {
|
||||
if (inputs.isEmpty()) {
|
||||
@@ -46,7 +52,7 @@ internal object StreamAudioMixer {
|
||||
audio.close(); output = OutputState(); worker = null
|
||||
return
|
||||
}
|
||||
inputs.toList()
|
||||
snapshot
|
||||
}
|
||||
audio.step(System.nanoTime(), hold = false, pull = {
|
||||
mix.clear()
|
||||
@@ -65,6 +71,7 @@ internal object StreamAudioMixer {
|
||||
synchronized(lock) {
|
||||
for (input in inputs) input.outputFailure = e.message ?: "Audio output failed"
|
||||
inputs.clear()
|
||||
snapshot = emptyList()
|
||||
try { audio.close() }
|
||||
catch (close: Exception) { LogManager.addLog("WARN", "stream", "mixer release: ${close.message}") }
|
||||
output = OutputState(); worker = null
|
||||
@@ -73,19 +80,21 @@ internal object StreamAudioMixer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Независимые Opus/SoundTouch, темп и PTS. Video worker и mixer сериализуют только свой DSP.
|
||||
* Каждый 10ms блок общего выхода отражён в своей шкале, включая тишину при starvation/hold.
|
||||
/** Независимые Opus/SoundTouch, темп и PTS. Audio worker готовит PCM вне lock; mixer потребляет SPSC.
|
||||
* Каждый 10ms блок общего выхода отражён в своей шкале, включая тишину при starvation.
|
||||
* Reset создаёт новый Input с новой origin; AudioTrack и остальные источники продолжают играть. */
|
||||
internal class StreamMixedPlayback(val id: Long, pts: Long, private val audioEnabled: Boolean, private val wake: () -> Unit) {
|
||||
private val lock = Any()
|
||||
private var handle = NativeLib.streamPlaybackCreate(id, pts, audioEnabled).also { check(it != 0L) }
|
||||
private val timeline = StreamMixTimeline()
|
||||
private var started = false
|
||||
private var hold = false
|
||||
private var ready = -1L
|
||||
private var ended = false
|
||||
private var endFrame: Long? = null
|
||||
private var fadeFrames = 0
|
||||
private val pcm = ShortArray(960)
|
||||
private var underruns = 0L
|
||||
private var silentFrames = 0L
|
||||
private var observedUnderruns = 0L
|
||||
@Volatile var outputFailure: String? = null
|
||||
init { if (audioEnabled) StreamAudioMixer.attach(this) }
|
||||
|
||||
@@ -98,31 +107,40 @@ internal class StreamMixedPlayback(val id: Long, pts: Long, private val audioEna
|
||||
}
|
||||
val description: String get() = synchronized(lock) {
|
||||
"mixed=$audioEnabled origin=${timeline.origin} written=${timeline.frames} " +
|
||||
"played=${timeline.played(StreamAudioMixer.output.played)} hold=$hold leadUs=$leadUs"
|
||||
"played=${timeline.played(StreamAudioMixer.output.played)} leadUs=$leadUs " +
|
||||
"pcmFrames=${NativeLib.streamPlaybackBuffered(handle)} underruns=$underruns silentFrames=$silentFrames"
|
||||
}
|
||||
|
||||
fun feed(pts: Long, packet: ByteArray): Int = synchronized(lock) {
|
||||
check(handle != 0L); NativeLib.streamPlaybackFeed(handle, pts, packet)
|
||||
// Только audio worker; он же закрывает handle, поэтому DSP не нуждается в consumer lock.
|
||||
fun prepare(): Int { check(handle != 0L); return NativeLib.streamPlaybackPrepare(handle) }
|
||||
fun feed(pts: Long, packet: ByteArray): Int {
|
||||
check(handle != 0L); return NativeLib.streamPlaybackFeed(handle, pts, packet)
|
||||
}
|
||||
fun finish() = synchronized(lock) {
|
||||
check(handle != 0L); ended = true; NativeLib.streamPlaybackFinish(handle)
|
||||
fun finish() {
|
||||
synchronized(lock) { check(handle != 0L); ended = true }
|
||||
NativeLib.streamPlaybackFinish(handle)
|
||||
}
|
||||
|
||||
fun update(now: Long, available: Long, guard: Long, videoHeld: Boolean): LongArray? = synchronized(lock) {
|
||||
fun update(now: Long, available: Long, guard: Long): LongArray? = synchronized(lock) {
|
||||
check(handle != 0L); check(outputFailure == null) { outputFailure!! }
|
||||
ready = available; hold = videoHeld; started = true
|
||||
NativeLib.streamPlaybackUpdate(handle, now, timeline.played(StreamAudioMixer.output.played), available, guard, videoHeld)
|
||||
started = true
|
||||
val starved = underruns != observedUnderruns
|
||||
observedUnderruns = underruns
|
||||
NativeLib.streamPlaybackUpdate(handle, now, timeline.played(StreamAudioMixer.output.played), available, guard, starved)
|
||||
}
|
||||
|
||||
/** Mixer не блокирует чужой input ожиданием media; недостающее заменяется тишиной только этого источника. */
|
||||
fun mix(buffer: StreamMixBuffer, firstOutputFrame: Long): Boolean = synchronized(lock) {
|
||||
if (handle == 0L || !started || !audioEnabled || outputFailure != null) return false
|
||||
try {
|
||||
val pcm = if (hold) null else NativeLib.streamPlaybackPull(handle, buffer.frames, if (ended) Long.MAX_VALUE else ready)
|
||||
val count = (pcm?.size ?: 0) / 2
|
||||
if (pcm != null) fadeFrames = buffer.add(pcm, fadeFrames)
|
||||
val count = NativeLib.streamPlaybackPull(handle, pcm, buffer.frames, Long.MAX_VALUE)
|
||||
check(count >= 0) { "Ready PCM pull failed" }
|
||||
fadeFrames = buffer.add(pcm, fadeFrames, count)
|
||||
if (ended && endFrame == null && NativeLib.streamPlaybackEmpty(handle)) endFrame = timeline.frames + count
|
||||
if (count < buffer.frames) NativeLib.streamPlaybackSilence(handle, buffer.frames - count)
|
||||
if (count < buffer.frames) {
|
||||
NativeLib.streamPlaybackSilence(handle, buffer.frames - count)
|
||||
if (!ended) { underruns++; silentFrames += buffer.frames - count }
|
||||
}
|
||||
timeline.append(firstOutputFrame, buffer.frames)
|
||||
wake()
|
||||
true
|
||||
|
||||
@@ -277,7 +277,7 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
if (!running) break
|
||||
video.step(position, System.nanoTime() / 1000)
|
||||
needsAudio = !audioInputEnded && (playback?.needsInput ?: true)
|
||||
val audioAhead = if (!audioInputEnded && (!audioEnabled || needsAudio) && records.audios == 0) deviceLead + 80000 else -1L
|
||||
val audioAhead = if (!audioInputEnded && (!audioEnabled || needsAudio) && records.audios == 0) deviceLead + 500000 else -1L
|
||||
val videoBudget = if (!videoInputEnded && video.wantsInput && records.videos == 0) 1 else 0
|
||||
val batch = if (ended) null else NativeLib.streamPull(id, position, videoBudget, audioAhead, 16)
|
||||
if (batch != null) { batches++; batchMax = maxOf(batchMax, records.add(batch)) }
|
||||
@@ -345,14 +345,13 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
if (!ended && (!opened || waitingKey) && System.nanoTime() > deadline) error("Viewer open timeout")
|
||||
if (!playbackStarted && video.firstOutputPts != null) playbackStarted = true
|
||||
if (playbackStarted && playback != null && ready >= 0) {
|
||||
val lateVideo = !video.ended && (video.firstOutputPts == null
|
||||
|| (video.oldestSubmittedPts >= 0 && video.oldestSubmittedPts < position - 10000))
|
||||
clockState = playback!!.update(System.nanoTime() / 1000,
|
||||
if (ended) ready.coerceAtLeast(position) else ready,
|
||||
maxOf(playback!!.leadUs + 10000, video.decoderLeadUs + video.framePeriodUs), lateVideo)
|
||||
maxOf(40000, playback!!.leadUs + 20000))
|
||||
check(clockState != null) { "Playback timeline failed" }
|
||||
position = clockState!![0]; needsAudio = clockState!![5] != 0L
|
||||
}
|
||||
check((playback?.prepare() ?: 0) >= 0) { "PCM preparation failed" }
|
||||
val codec = decoder
|
||||
if (codec != null) {
|
||||
if (videoInputEnded && video.input == null && video.output == null && !eosQueued) {
|
||||
|
||||
Reference in new issue
Block a user