299 lines
18 KiB
C
299 lines
18 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"
|
|
#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, 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
|