194 lines
9.9 KiB
C
194 lines
9.9 KiB
C
#include <pthread.h>
|
|
#include <string.h>
|
|
#include "../../../lib/debug_config.h"
|
|
#include "../../../lib/mem.h"
|
|
#include "../../../lib/u_async.h"
|
|
#include "../../../src/utun_instance.h"
|
|
#include "../../../src/stream/stream.h"
|
|
#include "../../../src/stream/stream_publisher.h"
|
|
#include "../../../src/stream/stream_viewer.h"
|
|
#include "stream_bridge.h"
|
|
|
|
static pthread_mutex_t media_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
static struct UTUN_INSTANCE *media_inst;
|
|
static struct stream_publisher *transmitter;
|
|
static struct stream_viewers *viewers;
|
|
static uint64_t publisher_id, watch_group;
|
|
static int publisher_opening;
|
|
static struct posted_task *watch_post;
|
|
|
|
/* Отложенная команда каталога; прочие команды обслуживают общие publisher/viewer managers. */
|
|
static void watch_process(void *arg) {
|
|
(void)arg;
|
|
pthread_mutex_lock(&media_lock); watch_post = NULL;
|
|
if (stream_watch_sources(media_inst, watch_group, 0) != STREAM_OK)
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android source watch failed group=%llx", (unsigned long long)watch_group);
|
|
pthread_mutex_unlock(&media_lock);
|
|
}
|
|
|
|
int android_stream_attach(struct UTUN_INSTANCE *inst) {
|
|
pthread_mutex_lock(&media_lock);
|
|
if (!inst || !inst->stream || (media_inst && media_inst != inst)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream attach rejected inst=%p owner=%p", (void *)inst, (void *)media_inst);
|
|
pthread_mutex_unlock(&media_lock); return -1;
|
|
}
|
|
if (media_inst == inst) { pthread_mutex_unlock(&media_lock); return 0; }
|
|
if (stream_publisher_create(&transmitter, inst) < 0) { pthread_mutex_unlock(&media_lock); return -1; }
|
|
if (stream_viewers_create(&viewers, inst) < 0) {
|
|
stream_publisher_destroy(transmitter); transmitter = NULL; pthread_mutex_unlock(&media_lock); return -1;
|
|
}
|
|
media_inst = inst;
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "Android stream bridge attached");
|
|
pthread_mutex_unlock(&media_lock); return 0;
|
|
}
|
|
|
|
void android_stream_detach(struct UTUN_INSTANCE *inst) {
|
|
pthread_mutex_lock(&media_lock);
|
|
if (media_inst == inst && inst) {
|
|
if (watch_post) uasync_cancel_post(inst->ua, watch_post);
|
|
watch_post = NULL; watch_group = 0; stream_watch_sources(inst, 0, 0);
|
|
stream_viewers_destroy(viewers); viewers = NULL;
|
|
stream_publisher_destroy(transmitter); transmitter = NULL;
|
|
publisher_id = 0; publisher_opening = 0; media_inst = NULL;
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "Android stream bridge detached; producers rejected");
|
|
}
|
|
pthread_mutex_unlock(&media_lock);
|
|
}
|
|
|
|
int android_stream_watch(uint64_t group) {
|
|
pthread_mutex_lock(&media_lock);
|
|
int result = -1;
|
|
if (!media_inst || !uasync_is_running(media_inst->ua))
|
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android source watch rejected group=%llx", (unsigned long long)group);
|
|
else {
|
|
if (!watch_post) {
|
|
watch_post = u_calloc(1, sizeof(*watch_post));
|
|
if (watch_post) { watch_post->callback = watch_process; uasync_post_reserved(media_inst->ua, watch_post); }
|
|
else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android source watch allocation failed");
|
|
}
|
|
if (watch_post) { watch_group = group; result = 0; }
|
|
}
|
|
pthread_mutex_unlock(&media_lock); return result;
|
|
}
|
|
|
|
/* Каждому viewer — собственный cursor. Publisher использует отдельное состояние. */
|
|
int android_stream_open(uint64_t id, uint64_t group, uint64_t source, unsigned width, unsigned height,
|
|
unsigned rotation, int audio, uint64_t epoch, unsigned codec) {
|
|
if (!id || !group || (source && !epoch) || codec < STREAM_MEDIA_H264 || codec > STREAM_MEDIA_H266
|
|
|| (!source && (!width || width > 4096 || !height || height > 4096 || rotation > 270 || rotation % 90))) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream open invalid id=%llu group=%llx source=%llx codec=%u",
|
|
(unsigned long long)id, (unsigned long long)group, (unsigned long long)source, codec); return -1;
|
|
}
|
|
pthread_mutex_lock(&media_lock);
|
|
int result = -1;
|
|
struct stream_media_status unused;
|
|
if (!media_inst || !uasync_is_running(media_inst->ua) || id == publisher_id
|
|
|| (!source && (publisher_id || stream_viewer_status(viewers, id, &unused) == 0)))
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream open busy/detached id=%llu", (unsigned long long)id);
|
|
else if (source)
|
|
result = stream_viewer_open(viewers, id, group, source, epoch, STREAM_MEDIA_TRACK_VIDEO | (audio ? STREAM_MEDIA_TRACK_AUDIO : 0));
|
|
else {
|
|
struct stream_media_format format = {(uint16_t)width, (uint16_t)height, (uint16_t)rotation,
|
|
(uint8_t)codec, audio ? STREAM_MEDIA_OPUS_STEREO : 0, 1};
|
|
result = stream_publisher_open(transmitter, id, group, &format);
|
|
if (!result) { publisher_id = id; publisher_opening = 1; }
|
|
}
|
|
pthread_mutex_unlock(&media_lock); return result;
|
|
}
|
|
|
|
int android_stream_tx_snapshot(uint64_t id, struct android_stream_tx_stats *out) {
|
|
if (!out) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "TX stats output missing"); return -1; }
|
|
pthread_mutex_lock(&media_lock);
|
|
struct stream_publisher_status status;
|
|
int result = stream_publisher_snapshot(transmitter, id, &status);
|
|
if (!result) *out = (struct android_stream_tx_stats){status.id, status.sample_us, status.video_bytes, status.audio_bytes,
|
|
status.stream_bytes, status.transport_bytes, status.inflight_bytes, status.acked_1s, status.total_bytes, status.connections};
|
|
pthread_mutex_unlock(&media_lock); return result;
|
|
}
|
|
|
|
int android_stream_feed(uint64_t id, int video, uint64_t pts, int key, const uint8_t *data, size_t length,
|
|
const struct stream_media_format *format) {
|
|
pthread_mutex_lock(&media_lock);
|
|
int result = stream_publisher_feed(transmitter, id, video, pts, key, data, length, format);
|
|
pthread_mutex_unlock(&media_lock); return result;
|
|
}
|
|
|
|
int android_stream_set_audio_enabled(uint64_t id, int enabled) {
|
|
pthread_mutex_lock(&media_lock);
|
|
int result = stream_publisher_set_audio_enabled(transmitter, id, enabled);
|
|
pthread_mutex_unlock(&media_lock); return result;
|
|
}
|
|
|
|
void android_stream_stop(uint64_t id) {
|
|
pthread_mutex_lock(&media_lock);
|
|
if (id == publisher_id && id) stream_publisher_stop(transmitter, id);
|
|
else stream_viewer_stop(viewers, id);
|
|
pthread_mutex_unlock(&media_lock);
|
|
}
|
|
|
|
/* Kotlin получает owned wire-копию только ближайшей записи своего reader. */
|
|
static uint8_t *encode_record(int kind, int flags, const struct stream_media_format *format, uint64_t pts,
|
|
const uint8_t *data, size_t bytes, size_t *length) {
|
|
uint8_t *p = u_calloc(1, ANDROID_STREAM_HEADER_SIZE + bytes);
|
|
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android output allocation failed bytes=%zu", bytes); return NULL; }
|
|
p[0] = (uint8_t)kind; p[1] = (uint8_t)flags;
|
|
if (format) {
|
|
stream_write_be(p + 2, format->width, 2); stream_write_be(p + 4, format->height, 2);
|
|
stream_write_be(p + 6, format->rotation, 2); stream_write_be(p + 20, format->revision, 4); p[24] = format->video_codec;
|
|
}
|
|
stream_write_be(p + 8, pts, 8); stream_write_be(p + 16, bytes, 4);
|
|
if (bytes) memcpy(p + ANDROID_STREAM_HEADER_SIZE, data, bytes);
|
|
*length = ANDROID_STREAM_HEADER_SIZE + bytes; return p;
|
|
}
|
|
|
|
uint8_t *android_stream_pull(uint64_t id, size_t *length) {
|
|
if (!length) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream pull missing length"); return NULL; }
|
|
*length = 0; pthread_mutex_lock(&media_lock);
|
|
uint8_t *data = NULL;
|
|
if (id == publisher_id && id) {
|
|
struct stream_publisher_status status;
|
|
if (stream_publisher_snapshot(transmitter, id, &status) == 0) {
|
|
if (status.state == STREAM_PUBLISHER_ENDED || status.state == STREAM_PUBLISHER_FAILED) {
|
|
data = encode_record(status.state == STREAM_PUBLISHER_ENDED ? 5 : 6, status.reason, NULL, 0, NULL, 0, length);
|
|
if (data) { publisher_id = 0; publisher_opening = 0; }
|
|
} else if (publisher_opening && status.state >= STREAM_PUBLISHER_LIVE && status.state <= STREAM_PUBLISHER_DRAINING) {
|
|
data = encode_record(4, 0, NULL, 0, NULL, 0, length);
|
|
if (data) publisher_opening = 0;
|
|
}
|
|
}
|
|
} else {
|
|
struct stream_viewer_record *r = stream_viewer_pull(viewers, id);
|
|
if (r) {
|
|
data = encode_record(r->kind, r->kind == STREAM_VIEWER_CLOSED ? r->reason : r->flags,
|
|
&r->format, r->pts, r->data, r->length, length);
|
|
if (!data) stream_viewer_resync(viewers, id);
|
|
u_free(r);
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&media_lock); return data;
|
|
}
|
|
|
|
int android_stream_read_request(uint64_t id, int64_t position, int want_video, int64_t audio_ahead) {
|
|
if ((want_video != 0 && want_video != 1) || audio_ahead < -1 || audio_ahead > 60000000) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android media demand invalid video=%d audio_ahead=%lld",
|
|
want_video, (long long)audio_ahead); return -1;
|
|
}
|
|
pthread_mutex_lock(&media_lock);
|
|
if (viewers && id != publisher_id) stream_viewer_request(viewers, id, position, want_video, audio_ahead, 1);
|
|
/* CLOSED/END остаются доступны pull даже после закрытия cursor; неизвестный ID даёт пустой pull. */
|
|
pthread_mutex_unlock(&media_lock); return 0;
|
|
}
|
|
|
|
int android_stream_get_status(uint64_t id, struct stream_media_status *status) {
|
|
if (!status) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android media status missing output"); return -1; }
|
|
pthread_mutex_lock(&media_lock);
|
|
int result = stream_viewer_status(viewers, id, status);
|
|
pthread_mutex_unlock(&media_lock); return result;
|
|
}
|
|
|
|
int android_stream_resync(uint64_t id) {
|
|
pthread_mutex_lock(&media_lock);
|
|
int result = stream_viewer_resync(viewers, id);
|
|
pthread_mutex_unlock(&media_lock); return result;
|
|
}
|