You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

283 lines
17 KiB

/* JNI live media: session IDs для ядра, codec handles принадлежат ровно одному audio worker. */
#ifdef __ANDROID__
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include "../../../lib/debug_config.h"
#include "../../../lib/mem.h"
#include "../../../lib/opus_codec.h"
#include "../../../lib/audio_compressor.h"
#include "../libutun_lite/stream_bridge.h"
#include "../../../src/stream/stream_media.h"
#include "../../../src/stream/stream_media_buffer.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 };
struct android_stream_opus {
opus_codec_encoder_t *encoder;
opus_codec_decoder_t *decoder;
struct audio_compressor *compressor;
int compressor_enabled, max_gain_db, rise_rate;
uint64_t frames;
uint32_t reported_clips;
};
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpen(
JNIEnv *env, jobject obj, jlong id, jlong group, jlong source, jint width, jint height, jint rotation, jboolean audio, jlong epoch) {
(void)env; (void)obj;
return android_stream_open((uint64_t)id, (uint64_t)group, (uint64_t)source, width, height, rotation, audio, (uint64_t)epoch) == 0;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamWatch(JNIEnv *env, jobject obj, jlong group) {
(void)env; (void)obj;
return android_stream_watch((uint64_t)group) == 0;
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamFeed(
JNIEnv *env, jobject obj, jlong id, jboolean video, jlong pts, jboolean key, jbyteArray data,
jlong revision, jint width, jint height, jint rotation, jboolean audio) {
(void)obj;
if (!data || pts < 0 || (video && (revision <= 0 || revision > UINT32_MAX || width <= 0 || width > 4096
|| height <= 0 || height > 4096 || rotation < 0 || rotation > 270 || rotation % 90))) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream feed missing data/invalid PTS"); return -1; }
jsize length = (*env)->GetArrayLength(env, data);
jbyte *bytes = (*env)->GetByteArrayElements(env, data, NULL);
if (!bytes) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream feed array access failed"); return -1; }
struct stream_media_format format = {(uint16_t)width, (uint16_t)height, (uint16_t)rotation,
STREAM_MEDIA_H264, audio ? STREAM_MEDIA_OPUS_STEREO : 0, (uint32_t)revision};
int result = android_stream_feed((uint64_t)id, video, (uint64_t)pts, key, (const uint8_t *)bytes, (size_t)length, video ? &format : NULL);
(*env)->ReleaseByteArrayElements(env, data, bytes, JNI_ABORT);
return result;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamStop(JNIEnv *env, jobject obj, jlong id) {
(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, 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);
if (output) (*env)->SetByteArrayRegion(env, output, 0, (jsize)length, (const jbyte *)data);
else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream pull array allocation failed bytes=%zu", length);
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;
int finished;
};
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);
if (!p->finished && stream_playback_audio_starved(p->audio, (uint64_t)played)) starved = JNI_TRUE;
} 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) { p->finished = 1; 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;
struct android_stream_opus *codec = u_calloc(1, sizeof(*codec));
if (!codec) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus allocation failed"); return 0; }
if (encode) {
codec->encoder = opus_codec_encoder_create(48000, STREAM_AUDIO_CHANNELS);
if (!codec->encoder || opus_codec_encoder_application_set(codec->encoder, OPUS_CODEC_APP_AUDIO) < 0
|| opus_codec_encoder_bitrate_set(codec->encoder, 128000) < 0) goto fail;
codec->compressor = audio_compressor_create();
if (!codec->compressor) goto fail;
} else {
codec->decoder = opus_codec_decoder_create(48000, STREAM_AUDIO_CHANNELS);
if (!codec->decoder) goto fail;
}
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "JNI stream Opus created codec=%p encode=%d rate=48000 channels=2 frame=960 bitrate=128000",
(void *)codec, encode);
return (jlong)(intptr_t)codec;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus initialization failed encode=%d", encode);
audio_compressor_destroy(codec->compressor);
opus_codec_encoder_destroy(codec->encoder); opus_codec_decoder_destroy(codec->decoder); u_free(codec); return 0;
}
JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusEncode(
JNIEnv *env, jobject obj, jlong handle, jshortArray samples, jboolean enabled, jint max_gain_db, jint rise_rate) {
(void)obj;
struct android_stream_opus *codec = (struct android_stream_opus *)(intptr_t)handle;
if (!codec || !codec->encoder || !codec->compressor || !samples || (*env)->GetArrayLength(env, samples) != STREAM_AUDIO_SAMPLES) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus encode invalid handle/frame"); return NULL;
}
if (max_gain_db < 1 || max_gain_db > 60 || rise_rate < 1 || rise_rate > 100) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream compressor invalid max_gain=%d rise=%d", max_gain_db, rise_rate); return NULL;
}
if (codec->compressor_enabled != !!enabled || codec->max_gain_db != max_gain_db || codec->rise_rate != rise_rate) {
audio_compressor_config_t config = {0};
config.sample_rate = 48000; config.channels = STREAM_AUDIO_CHANNELS; config.block_duration_ms = 20;
config.lookback_ms = 200; config.lookahead_ms = 0; config.target_level = 0.25f;
config.max_gain_db = max_gain_db; config.rise_rate_per_sec = rise_rate; config.release_rate_per_sec = rise_rate * 5.0f;
if (audio_compressor_configure(codec->compressor, &config) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream compressor configuration failed codec=%p", (void *)codec); return NULL;
}
audio_compressor_set_enabled(codec->compressor, enabled);
codec->compressor_enabled = !!enabled; codec->max_gain_db = max_gain_db; codec->rise_rate = rise_rate; codec->reported_clips = 0;
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
"JNI stream compressor applied codec=%p enabled=%d max_gain=%ddB rise=%d release=%d target=0.25 history=200ms delay=0ms",
(void *)codec, !!enabled, max_gain_db, rise_rate, rise_rate * 5);
}
int16_t pcm[STREAM_AUDIO_SAMPLES]; uint8_t packet[1275];
(*env)->GetShortArrayRegion(env, samples, 0, STREAM_AUDIO_SAMPLES, pcm);
if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream PCM read failed"); return NULL; }
int report = codec->frames % 50 == 0, input_peak = 0, output_peak = 0, stereo_difference = 0;
if (report) {
for (unsigned i = 0; i < STREAM_AUDIO_SAMPLES; i++) if (abs((int)pcm[i]) > input_peak) input_peak = abs((int)pcm[i]);
for (unsigned i = 0; i < STREAM_AUDIO_FRAMES; i++) stereo_difference += abs((int)pcm[2 * i] - pcm[2 * i + 1]);
}
if (audio_compressor_process_frame(codec->compressor, pcm, pcm, STREAM_AUDIO_SAMPLES) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream compressor processing failed codec=%p", (void *)codec); return NULL;
}
int length = opus_codec_encode(codec->encoder, pcm, STREAM_AUDIO_FRAMES, packet, sizeof(packet));
if (length < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus encode failed result=%d", length); return NULL; }
codec->frames++;
if (report) {
for (unsigned i = 0; i < STREAM_AUDIO_SAMPLES; i++) if (abs((int)pcm[i]) > output_peak) output_peak = abs((int)pcm[i]);
uint32_t clips = audio_compressor_clip_count(codec->compressor);
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM,
"JNI stream audio codec=%p frames=%llu channels=2 enabled=%d gain=%.1fdB peaks=%d->%d lrDifference=%d clips=%u packet=%d",
(void *)codec, (unsigned long long)codec->frames, codec->compressor_enabled,
audio_compressor_gain_db(codec->compressor), input_peak, output_peak, stereo_difference / STREAM_AUDIO_FRAMES, clips, length);
if (clips != codec->reported_clips) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "JNI stream audio clipped codec=%p samples=%u gain=%.1fdB",
(void *)codec, clips - codec->reported_clips, audio_compressor_gain_db(codec->compressor));
codec->reported_clips = clips;
}
jbyteArray output = (*env)->NewByteArray(env, length);
if (output) (*env)->SetByteArrayRegion(env, output, 0, length, (const jbyte *)packet);
else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus output allocation failed");
return output;
}
JNIEXPORT jshortArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusDecode(
JNIEnv *env, jobject obj, jlong handle, jbyteArray packet) {
(void)obj;
struct android_stream_opus *codec = (struct android_stream_opus *)(intptr_t)handle;
if (!codec || !codec->decoder || !packet || (*env)->GetArrayLength(env, packet) > 1275) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus decode invalid handle/packet"); return NULL;
}
jsize length = (*env)->GetArrayLength(env, packet); uint8_t bytes[1275]; int16_t pcm[STREAM_AUDIO_SAMPLES];
(*env)->GetByteArrayRegion(env, packet, 0, length, (jbyte *)bytes);
if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus packet read failed"); return NULL; }
int count = opus_codec_decode(codec->decoder, length ? bytes : NULL, length, pcm, STREAM_AUDIO_FRAMES);
if (count != STREAM_AUDIO_FRAMES) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus decode failed count=%d", count); return NULL; }
jshortArray output = (*env)->NewShortArray(env, STREAM_AUDIO_SAMPLES);
if (output) (*env)->SetShortArrayRegion(env, output, 0, STREAM_AUDIO_SAMPLES, pcm);
else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus PCM allocation failed");
return output;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusClose(JNIEnv *env, jobject obj, jlong handle) {
(void)env; (void)obj;
struct android_stream_opus *codec = (struct android_stream_opus *)(intptr_t)handle;
if (!codec) return;
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "JNI stream Opus release codec=%p encode=%d frames=%llu", (void *)codec,
codec->encoder != NULL, (unsigned long long)codec->frames);
audio_compressor_destroy(codec->compressor);
opus_codec_encoder_destroy(codec->encoder); opus_codec_decoder_destroy(codec->decoder); u_free(codec);
}
#endif