From e6e276aed37b553756dd33f2187a05c630a8a7bf Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 13 Oct 2026 23:03:29 +0200 Subject: [PATCH] Expose shared playback timing and stereo time stretch to Android --- src/stream/stream.c | 6 +- src/stream/stream_playback_clock.c | 4 +- .../jni_bridge/android_stream_jni.c | 110 +++++++++++++++++- 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/stream/stream.c b/src/stream/stream.c index 07c90f1c..f57ed92b 100644 --- a/src/stream/stream.c +++ b/src/stream/stream.c @@ -676,9 +676,6 @@ int stream_publish_open(struct UTUN_INSTANCE *inst, uint64_t group_id, const str struct STREAM_HANDLE **out) { int result = stream_open_check(inst, group_id, out); if (result != STREAM_OK) return result; - struct stream_session *previous = session_find(inst->stream, group_id, inst->node_id); - if (previous && previous->source && previous->end_valid) - session_close(previous, STREAM_CLOSE_SOURCE_CHANGED, 0, 1); struct stream_descriptor d = {.group_id = group_id, .source_node_id = inst->node_id, .n = parameters ? parameters->n : STREAM_DEFAULT_N, .r = parameters ? parameters->r : STREAM_DEFAULT_R, .block_size = parameters ? parameters->block_size : STREAM_RS_DEFAULT_SIZE}; @@ -688,6 +685,9 @@ int stream_publish_open(struct UTUN_INSTANCE *inst, uint64_t group_id, const str } } while (!d.source_epoch); if (stream_descriptor_validate(&d) != STREAM_OK) return STREAM_ERR_ARGUMENT; + struct stream_session *previous = session_find(inst->stream, group_id, inst->node_id); + if (previous && previous->source && previous->end_valid) + session_close(previous, STREAM_CLOSE_SOURCE_CHANGED, 0, 1); return session_open(inst, &d, inst->my_ed25519_pubkey, 1, 0, 0, out); } diff --git a/src/stream/stream_playback_clock.c b/src/stream/stream_playback_clock.c index ae468c35..29eea7dd 100644 --- a/src/stream/stream_playback_clock.c +++ b/src/stream/stream_playback_clock.c @@ -16,8 +16,8 @@ void stream_playback_clock_reset(struct stream_playback_clock *c, uint64_t id) { int stream_playback_clock_update(struct stream_playback_clock *c, int64_t now, int64_t position, int64_t ready, int64_t guard, int64_t pending_gain, int starved, struct stream_playback_state *state) { - if (!c || !state || now < 0 || position < 0 || ready < 0 || guard < 0 || guard > 1000000 - || pending_gain < 0 || pending_gain > 1000000 || (c->initialized && (now < c->last_us || position < c->last_position_us))) { + if (!c || !state || now < 0 || position < 0 || ready < 0 || guard < 0 || guard > 60000000 + || pending_gain < 0 || pending_gain > 60000000 || (c->initialized && (now < c->last_us || position < c->last_position_us))) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "playback clock invalid id=%llu now=%lld pts=%lld ready=%lld guard=%lld pending=%lld", c ? (unsigned long long)c->id : 0, (long long)now, (long long)position, (long long)ready, (long long)guard, (long long)pending_gain); diff --git a/tools/chatgui-android/jni_bridge/android_stream_jni.c b/tools/chatgui-android/jni_bridge/android_stream_jni.c index 589124ca..99c871fc 100644 --- a/tools/chatgui-android/jni_bridge/android_stream_jni.c +++ b/tools/chatgui-android/jni_bridge/android_stream_jni.c @@ -10,6 +10,8 @@ #include "../../../lib/audio_compressor.h" #include "../libutun_lite/stream_bridge.h" #include "../../../src/stream/stream_media.h" +#include "../../../src/stream/stream_playback_clock.h" +#include "../../../src/stream/stream_playback_audio.h" enum { STREAM_AUDIO_CHANNELS = 2, STREAM_AUDIO_FRAMES = 960, STREAM_AUDIO_SAMPLES = 1920 }; @@ -53,8 +55,10 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamStop(JNIEnv (void)env; (void)obj; android_stream_stop((uint64_t)id); } -JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPull(JNIEnv *env, jobject obj, jlong id) { +JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPull( + JNIEnv *env, jobject obj, jlong id, jlong position, jlong video_ahead, jlong audio_ahead) { (void)obj; + android_stream_read_request((uint64_t)id, position, video_ahead, audio_ahead); 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); @@ -63,6 +67,110 @@ JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPull( u_free(data); return output; } +JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamStatus(JNIEnv *env, jobject obj, jlong id) { + (void)obj; + struct stream_media_status status; + if (android_stream_get_status((uint64_t)id, &status) < 0) return NULL; + jlong values[] = {status.ready_us, status.video_us, status.audio_us, status.start_us, (jlong)status.generation, + status.initialized | (status.audio_enabled << 1) | (status.finished << 2) | (status.video_ended << 3) | (status.audio_ended << 4)}; + jlongArray result = (*env)->NewLongArray(env, 6); + if (result) (*env)->SetLongArrayRegion(env, result, 0, 6, values); + else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI media status allocation failed"); + return result; +} + +JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamResync(JNIEnv *env, jobject obj, jlong id) { + (void)env; (void)obj; return android_stream_resync((uint64_t)id) == 0; +} + +struct android_stream_playback { + struct stream_playback_clock clock; + struct stream_playback_state state; + struct stream_playback_audio *audio; + int64_t position, time_us; +}; + +JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackCreate( + JNIEnv *env, jobject obj, jlong id, jlong pts, jboolean audio) { + (void)env; (void)obj; + if (pts < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback invalid start PTS"); return 0; } + 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))) { u_free(p); return 0; } + return (jlong)(intptr_t)p; +} + +JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackFeed( + JNIEnv *env, jobject obj, jlong handle, jlong pts, jbyteArray packet) { + (void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle; + if (!p || !p->audio || !packet) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback feed invalid handle/packet"); return -1; } + jsize length = (*env)->GetArrayLength(env, packet); uint8_t bytes[1275]; + if (length <= 0 || length > (jsize)sizeof(bytes)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback packet size=%d", length); return -1; } + (*env)->GetByteArrayRegion(env, packet, 0, length, (jbyte *)bytes); + if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback packet access failed"); return -1; } + 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) { + (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 (limit < 0) return NULL; + int16_t pcm[8192 * 2]; + int count = stream_playback_audio_pull(p->audio, pcm, (unsigned)frames, limit); + 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; +} + +JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackUpdate( + JNIEnv *env, jobject obj, jlong handle, jlong now, jlong played, jlong ready, jlong guard, jboolean starved) { + (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); + pending = stream_playback_audio_pending_gain(p->audio, (uint64_t)played); + } else if (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; + if (proposed > ready) { proposed = ready; starved = JNI_TRUE; } + if (proposed > p->position) p->position = proposed; + } + 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}; + jlongArray output = (*env)->NewLongArray(env, 7); + if (output) (*env)->SetLongArrayRegion(env, output, 0, 7, values); + else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI playback clock allocation failed"); + return output; +} + +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) stream_playback_audio_finish(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); +} + +JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlaybackClose(JNIEnv *env, jobject obj, jlong handle) { + (void)env; (void)obj; struct android_stream_playback *p = (struct android_stream_playback *)(intptr_t)handle; + if (!p) return; + stream_playback_audio_destroy(p->audio); u_free(p); +} + /* Opus state существует только в audio worker; encode/decode не занимают uasync. */ JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusCreate(JNIEnv *env, jobject obj, jboolean encode) { (void)env; (void)obj;