173 lines
11 KiB
C
173 lines
11 KiB
C
/* 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"
|
|
|
|
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) {
|
|
(void)obj;
|
|
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;
|
|
}
|
|
|
|
/* 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
|