Browse Source

Expose shared playback timing and stereo time stretch to Android

master
evgeny 5 days ago
parent
commit
e6e276aed3
  1. 6
      src/stream/stream.c
  2. 4
      src/stream/stream_playback_clock.c
  3. 110
      tools/chatgui-android/jni_bridge/android_stream_jni.c

6
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);
}

4
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);

110
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;

Loading…
Cancel
Save