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) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* JNI live media: session IDs для ядра. Playback handles сериализуются своим StreamMixedPlayback lock
|
||||
* между video worker и общим микшером; capture Opus handles принадлежат одному audio worker. */
|
||||
/* JNI live media: session IDs для ядра. Playback DSP принадлежит audio worker; mixer берёт готовый PCM.
|
||||
* Только output map/clock сериализуются StreamMixedPlayback lock; release выполняет audio worker после detach. */
|
||||
#ifdef __ANDROID__
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "../../../lib/debug_config.h"
|
||||
#include "../../../lib/mem.h"
|
||||
@@ -124,7 +125,7 @@ struct android_stream_playback {
|
||||
struct stream_playback_state state;
|
||||
struct stream_playback_audio *audio;
|
||||
int64_t position, time_us;
|
||||
int finished;
|
||||
atomic_int finished;
|
||||
uint64_t end_frame;
|
||||
};
|
||||
|
||||
@@ -135,7 +136,8 @@ JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackCr
|
||||
struct android_stream_playback *p = u_calloc(1, sizeof(*p));
|
||||
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback allocation failed"); return 0; }
|
||||
stream_playback_clock_reset(&p->clock, (uint64_t)id); p->position = pts; p->time_us = -1;
|
||||
if (audio && !(p->audio = stream_playback_audio_create(pts, 40))) { u_free(p); return 0; }
|
||||
atomic_init(&p->finished, 0);
|
||||
if (audio && !(p->audio = stream_playback_audio_create(pts, 250))) { u_free(p); return 0; }
|
||||
return (jlong)(intptr_t)p;
|
||||
}
|
||||
|
||||
@@ -150,22 +152,34 @@ JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackFee
|
||||
return stream_playback_audio_feed(p->audio, pts, bytes, (size_t)length);
|
||||
}
|
||||
|
||||
JNIEXPORT jshortArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackPull(
|
||||
JNIEnv *env, jobject obj, jlong handle, jint frames, jlong limit) {
|
||||
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackPrepare(JNIEnv *env, jobject obj, jlong handle) {
|
||||
(void)env; (void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle;
|
||||
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback prepare invalid handle"); return -1; }
|
||||
if (!p->audio) return 0;
|
||||
stream_playback_audio_set_tempo(p->audio, p->clock.tempo_milli);
|
||||
return stream_playback_audio_prepare(p->audio);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackBuffered(JNIEnv *env, jobject obj, jlong handle) {
|
||||
(void)env; (void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle;
|
||||
return p && p->audio ? (jint)stream_playback_audio_buffered(p->audio) : 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackPull(
|
||||
JNIEnv *env, jobject obj, jlong handle, jshortArray output, jint frames, jlong limit) {
|
||||
(void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle;
|
||||
if (!p || !p->audio || frames <= 0 || frames > 8192) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback PCM invalid frames=%d", frames); return NULL;
|
||||
if (!p || !p->audio || !output || frames <= 0 || frames > 8192 || (*env)->GetArrayLength(env, output) < frames * 2) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback PCM invalid frames=%d", frames); return -1;
|
||||
}
|
||||
if (limit < 0) return NULL;
|
||||
if (limit < 0) return 0;
|
||||
int16_t pcm[8192 * 2];
|
||||
int count = stream_playback_audio_pull(p->audio, pcm, (unsigned)frames, limit);
|
||||
if (p->finished && !p->end_frame && stream_playback_audio_empty(p->audio))
|
||||
p->end_frame = stream_playback_audio_written(p->audio) + 1; /* 0 — конец ещё не поставлен устройству. */
|
||||
if (count <= 0) return NULL;
|
||||
jshortArray output = (*env)->NewShortArray(env, count * 2);
|
||||
if (output) (*env)->SetShortArrayRegion(env, output, 0, count * 2, pcm);
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback PCM allocation failed frames=%d", count);
|
||||
return output;
|
||||
if (count <= 0) return count;
|
||||
(*env)->SetShortArrayRegion(env, output, 0, count * 2, pcm);
|
||||
if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback PCM copy failed"); return -1; }
|
||||
return count;
|
||||
}
|
||||
|
||||
JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackUpdate(
|
||||
@@ -178,7 +192,6 @@ JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlayb
|
||||
int64_t position = stream_playback_audio_position(p->audio, (uint64_t)played);
|
||||
if (position > p->position) p->position = position;
|
||||
pending = stream_playback_audio_pending_gain(p->audio, (uint64_t)played);
|
||||
if (!p->finished && stream_playback_audio_starved(p->audio, (uint64_t)played)) starved = JNI_TRUE;
|
||||
}
|
||||
if ((!p->audio || audio_done) && p->time_us >= 0 && !starved) {
|
||||
int64_t elapsed = now - p->time_us;
|
||||
@@ -189,7 +202,6 @@ JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlayb
|
||||
}
|
||||
p->time_us = now;
|
||||
if (stream_playback_clock_update(&p->clock, now, p->position, ready, guard, pending, starved, &p->state) < 0) return NULL;
|
||||
if (p->audio) stream_playback_audio_set_tempo(p->audio, p->state.tempo_milli);
|
||||
jlong values[] = {p->position, p->state.reserve_us, p->state.minimum_us, p->state.tempo_milli, p->state.waiting,
|
||||
p->audio ? stream_playback_audio_needs_input(p->audio) : 0,
|
||||
p->audio ? (jlong)stream_playback_audio_written(p->audio) : 0};
|
||||
@@ -210,7 +222,7 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackSil
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackFinish(JNIEnv *env, jobject obj, jlong handle) {
|
||||
(void)env; (void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle;
|
||||
if (p) { p->finished = 1; stream_playback_audio_finish(p->audio); }
|
||||
if (p) { stream_playback_audio_finish(p->audio); p->finished = 1; }
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackNeedsInput(JNIEnv *env, jobject obj, jlong handle) {
|
||||
|
||||
Reference in new issue
Block a user