/* JNI live media: session IDs для ядра, codec handles принадлежат ровно одному audio worker. */ #ifdef __ANDROID__ #include #include #include #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, jint codec) { (void)env; (void)obj; return android_stream_open((uint64_t)id, (uint64_t)group, (uint64_t)source, width, height, rotation, audio, (uint64_t)epoch, codec) == 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, jint video_codec) { (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, (uint8_t)video_codec, audio ? STREAM_MEDIA_OPUS_STEREO : 0, (uint32_t)revision}; if (video && (video_codec < STREAM_MEDIA_H264 || video_codec > STREAM_MEDIA_H266)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI video codec invalid id=%d", video_codec); (*env)->ReleaseByteArrayElements(env, data, bytes, JNI_ABORT); return -1; } 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 jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamTxStats(JNIEnv *env, jobject obj, jlong id) { (void)obj; struct android_stream_tx_stats s; if (android_stream_tx_snapshot((uint64_t)id, &s) < 0) return NULL; jlong values[] = {s.sample_us, s.video_bytes, s.audio_bytes, s.stream_bytes, s.transport_bytes, s.inflight_bytes, s.acked_1s, s.total_bytes, s.connections}; jlongArray result = (*env)->NewLongArray(env, 9); if (result) (*env)->SetLongArrayRegion(env, result, 0, 9, values); else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI TX stats allocation failed"); return result; } JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPull( JNIEnv *env, jobject obj, jlong id, jlong position, jboolean want_video, jlong audio_ahead) { (void)obj; if (android_stream_read_request((uint64_t)id, position, want_video != JNI_FALSE, audio_ahead) < 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); 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