Expose viewer wake signals and complete audio independently on Android
This commit is contained in:
1 parent
9fb4823df0
commit
a538f99bf0
3 files changed
+42
-9
No files matched your search
@@ -215,10 +215,14 @@ object NativeLib {
|
||||
fun streamTxStats(id: Long): LongArray? = nativeStreamTxStats(id)
|
||||
fun streamSetAudioEnabled(id: Long, enabled: Boolean): Boolean = nativeStreamSetAudioEnabled(id, enabled)
|
||||
fun streamStop(id: Long) { nativeStreamStop(id) }
|
||||
fun streamPull(id: Long, positionUs: Long = -1, wantVideo: Boolean = true, audioAheadUs: Long = 80000): ByteArray? =
|
||||
nativeStreamPull(id, positionUs, wantVideo, audioAheadUs)
|
||||
fun streamPull(id: Long, positionUs: Long = -1, video: Int = 1, audioAheadUs: Long = 80000, records: Int = 1): ByteArray? =
|
||||
nativeStreamPull(id, positionUs, video, audioAheadUs, records)
|
||||
fun streamStatus(id: Long): LongArray? = nativeStreamStatus(id)
|
||||
fun streamResync(id: Long): Boolean = nativeStreamResync(id)
|
||||
external fun streamSignalCreate(id: Long): Long
|
||||
external fun streamSignalNotify(handle: Long)
|
||||
external fun streamSignalWait(handle: Long, timeoutMs: Int): Int
|
||||
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)
|
||||
@@ -227,6 +231,7 @@ object NativeLib {
|
||||
nativeStreamPlaybackUpdate(handle, now, played, ready, guard, starved)
|
||||
fun streamPlaybackFinish(handle: Long) { nativeStreamPlaybackFinish(handle) }
|
||||
fun streamPlaybackEmpty(handle: Long): Boolean = nativeStreamPlaybackEmpty(handle)
|
||||
fun streamPlaybackNeedsInput(handle: Long): Boolean = nativeStreamPlaybackNeedsInput(handle)
|
||||
fun streamPlaybackClose(handle: Long) { nativeStreamPlaybackClose(handle) }
|
||||
fun streamOpusCreate(encode: Boolean): Long = nativeStreamOpusCreate(encode)
|
||||
fun streamOpusEncode(handle: Long, pcm: ShortArray, compressor: RadioCompressorSettings): ByteArray? =
|
||||
@@ -241,7 +246,7 @@ object NativeLib {
|
||||
private external fun nativeStreamTxStats(id: Long): LongArray?
|
||||
private external fun nativeStreamSetAudioEnabled(id: Long, enabled: Boolean): Boolean
|
||||
private external fun nativeStreamStop(id: Long)
|
||||
private external fun nativeStreamPull(id: Long, position: Long, wantVideo: Boolean, audioAhead: Long): ByteArray?
|
||||
private external fun nativeStreamPull(id: Long, position: Long, video: Int, audioAhead: Long, records: Int): ByteArray?
|
||||
private external fun nativeStreamStatus(id: Long): LongArray?
|
||||
private external fun nativeStreamResync(id: Long): Boolean
|
||||
private external fun nativeStreamPlaybackCreate(id: Long, pts: Long, audio: Boolean): Long
|
||||
@@ -252,6 +257,7 @@ object NativeLib {
|
||||
guard: Long, starved: Boolean): LongArray?
|
||||
private external fun nativeStreamPlaybackFinish(handle: Long)
|
||||
private external fun nativeStreamPlaybackEmpty(handle: Long): Boolean
|
||||
private external fun nativeStreamPlaybackNeedsInput(handle: Long): Boolean
|
||||
private external fun nativeStreamPlaybackClose(handle: Long)
|
||||
private external fun nativeStreamOpusCreate(encode: Boolean): Long
|
||||
private external fun nativeStreamOpusEncode(handle: Long, pcm: ShortArray, compressor: Boolean, maxGainDb: Int, riseRate: Int): ByteArray?
|
||||
|
||||
@@ -76,7 +76,7 @@ internal object StreamAudioMixer {
|
||||
/** Независимые Opus/SoundTouch, темп и PTS. Video worker и mixer сериализуют только свой DSP.
|
||||
* Каждый 10ms блок общего выхода отражён в своей шкале, включая тишину при starvation/hold.
|
||||
* Reset создаёт новый Input с новой origin; AudioTrack и остальные источники продолжают играть. */
|
||||
internal class StreamMixedPlayback(val id: Long, pts: Long, private val audioEnabled: Boolean) {
|
||||
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()
|
||||
@@ -91,6 +91,7 @@ internal class StreamMixedPlayback(val id: Long, pts: Long, private val audioEna
|
||||
|
||||
val leadUs: Long get() = if (audioEnabled) StreamAudioMixer.output.leadUs else 0
|
||||
val empty: Boolean get() = synchronized(lock) { handle == 0L || NativeLib.streamPlaybackEmpty(handle) }
|
||||
val needsInput: Boolean get() = synchronized(lock) { handle != 0L && NativeLib.streamPlaybackNeedsInput(handle) }
|
||||
val drained: Boolean get() = synchronized(lock) {
|
||||
!audioEnabled || (ended && (endFrame?.let { timeline.played(StreamAudioMixer.output.played) >= it }
|
||||
?: (timeline.frames == 0L && NativeLib.streamPlaybackEmpty(handle))))
|
||||
@@ -123,9 +124,11 @@ internal class StreamMixedPlayback(val id: Long, pts: Long, private val audioEna
|
||||
if (ended && endFrame == null && NativeLib.streamPlaybackEmpty(handle)) endFrame = timeline.frames + count
|
||||
if (count < buffer.frames) NativeLib.streamPlaybackSilence(handle, buffer.frames - count)
|
||||
timeline.append(firstOutputFrame, buffer.frames)
|
||||
wake()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
outputFailure = e.message ?: "Audio mixing failed"
|
||||
wake()
|
||||
LogManager.addLog("ERROR", "stream", "mixer input failed id=$id: ${e.message}")
|
||||
false
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamTxSta
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPull(
|
||||
JNIEnv *env, jobject obj, jlong id, jlong position, jboolean want_video, jlong audio_ahead) {
|
||||
JNIEnv *env, jobject obj, jlong id, jlong position, jint video, jlong audio_ahead, jint records) {
|
||||
(void)obj;
|
||||
if (android_stream_read_request((uint64_t)id, position, want_video != JNI_FALSE, audio_ahead) < 0) return NULL;
|
||||
if (android_stream_read_request((uint64_t)id, position, (unsigned)video, audio_ahead, (unsigned)records) < 0) return NULL;
|
||||
size_t length = 0; uint8_t *data = android_stream_pull((uint64_t)id, &length);
|
||||
if (!data) return NULL;
|
||||
jbyteArray output = (*env)->NewByteArray(env, (jsize)length);
|
||||
@@ -106,12 +106,26 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamResync(
|
||||
(void)env; (void)obj; return android_stream_resync((uint64_t)id) == 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_streamSignalCreate(JNIEnv *env, jobject obj, jlong id) {
|
||||
(void)env; (void)obj; return (jlong)(intptr_t)android_stream_signal_create((uint64_t)id);
|
||||
}
|
||||
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_streamSignalNotify(JNIEnv *env, jobject obj, jlong handle) {
|
||||
(void)env; (void)obj; android_stream_signal_notify((struct android_stream_signal *)(intptr_t)handle);
|
||||
}
|
||||
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_streamSignalWait(JNIEnv *env, jobject obj, jlong handle, jint ms) {
|
||||
(void)env; (void)obj; return android_stream_signal_wait((struct android_stream_signal *)(intptr_t)handle, (unsigned)ms);
|
||||
}
|
||||
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_streamSignalClose(JNIEnv *env, jobject obj, jlong handle) {
|
||||
(void)env; (void)obj; android_stream_signal_destroy((struct android_stream_signal *)(intptr_t)handle);
|
||||
}
|
||||
|
||||
struct android_stream_playback {
|
||||
struct stream_playback_clock clock;
|
||||
struct stream_playback_state state;
|
||||
struct stream_playback_audio *audio;
|
||||
int64_t position, time_us;
|
||||
int finished;
|
||||
uint64_t end_frame;
|
||||
};
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackCreate(
|
||||
@@ -145,6 +159,8 @@ JNIEXPORT jshortArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlay
|
||||
if (limit < 0) return NULL;
|
||||
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);
|
||||
@@ -157,11 +173,14 @@ JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlayb
|
||||
(void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle;
|
||||
if (!p || now < 0 || played < 0 || ready < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback clock invalid input"); return NULL; }
|
||||
int64_t pending = 0;
|
||||
if (p->audio) {
|
||||
p->position = stream_playback_audio_position(p->audio, (uint64_t)played);
|
||||
int audio_done = p->finished && p->end_frame && (uint64_t)played >= p->end_frame - 1;
|
||||
if (p->audio && !audio_done) {
|
||||
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;
|
||||
} else if (p->time_us >= 0 && !starved) {
|
||||
}
|
||||
if ((!p->audio || audio_done) && p->time_us >= 0 && !starved) {
|
||||
int64_t elapsed = now - p->time_us;
|
||||
if (elapsed < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback wall clock regressed"); return NULL; }
|
||||
int64_t proposed = p->position + elapsed * p->clock.tempo_milli / 1000;
|
||||
@@ -194,6 +213,11 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackFin
|
||||
if (p) { p->finished = 1; stream_playback_audio_finish(p->audio); }
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackNeedsInput(JNIEnv *env, jobject obj, jlong handle) {
|
||||
(void)env; (void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle;
|
||||
return p && stream_playback_audio_needs_input(p->audio);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackEmpty(JNIEnv *env, jobject obj, jlong handle) {
|
||||
(void)env; (void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle;
|
||||
return !p || stream_playback_audio_empty(p->audio);
|
||||
|
||||
Reference in new issue
Block a user