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.
439 lines
25 KiB
439 lines
25 KiB
#include <pthread.h> |
|
#include <stdio.h> |
|
#include <string.h> |
|
|
|
#include "../../../lib/debug_config.h" |
|
#include "../../../lib/mem.h" |
|
#include "../../../lib/u_async.h" |
|
#include "../../../lib/sqlite3.h" |
|
#include "../../../lib/strbuf.h" |
|
#include "../../../src/utun_instance.h" |
|
#include "../../../src/chat/chat_core.h" |
|
#include "../../../src/routing_layer/topo_group.h" |
|
#include "../../../src/stream/stream.h" |
|
#include "../../../src/stream/stream_member.h" |
|
#include "../../../src/stream/stream_media.h" |
|
#include "stream_bridge.h" |
|
|
|
#define TX_LIMIT (2u * 1024u * 1024u) |
|
#define RX_LIMIT (4u * 1024u * 1024u) |
|
#define TX_RECORDS 128 |
|
#define RX_RECORDS 256 |
|
#define MEDIA_AGE_US 250000u |
|
|
|
struct media_record { |
|
struct media_record *next; |
|
uint64_t id, pts, frame_id, created; |
|
size_t length, offset; |
|
int key; |
|
uint8_t data[]; |
|
}; |
|
struct media_queue { struct media_record *head, *tail; size_t bytes; unsigned count; }; |
|
struct android_media_session { |
|
uint64_t id, group, source, epoch, expected_epoch, next_frame, last_pts[2], drain_until; |
|
struct STREAM_HANDLE *handle; |
|
struct stream_media_receiver *receiver; |
|
struct stream_media_format format; |
|
int opening, stopping, finished, need_key, seen[2], rx_overflow, reason; |
|
}; |
|
|
|
static pthread_mutex_t media_lock = PTHREAD_MUTEX_INITIALIZER; |
|
static struct UTUN_INSTANCE *media_inst; |
|
static struct posted_task *media_post; |
|
static void *media_timer; |
|
static struct android_media_session publisher, viewer; |
|
static struct media_queue video_queue, audio_queue, output_queue; |
|
static uint64_t list_group, cached_group, list_updated; |
|
static uint64_t watch_group; |
|
static int watch_pending; |
|
static char *source_list; |
|
static void media_process(void *arg); |
|
static void media_close(struct android_media_session *s, int kind); |
|
|
|
/* Очереди обмена pthread: LL_QUEUE принадлежит uasync и сюда не подходит. */ |
|
static void queue_append(struct media_queue *q, struct media_record *record) { |
|
record->next = NULL; |
|
if (q->tail) q->tail->next = record; else q->head = record; |
|
q->tail = record; q->bytes += record->length; q->count++; |
|
} |
|
static struct media_record *queue_take(struct media_queue *q) { |
|
struct media_record *record = q->head; |
|
if (!record) return NULL; |
|
q->head = record->next; if (!q->head) q->tail = NULL; |
|
q->bytes -= record->length; q->count--; record->next = NULL; |
|
return record; |
|
} |
|
static void queue_clear(struct media_queue *q) { |
|
struct media_record *record; |
|
while ((record = queue_take(q))) u_free(record); |
|
} |
|
|
|
/* RX pressure удаляет media/GAP, сохраняя подтверждения открытия/закрытия owners. */ |
|
static void media_drop_output(void) { |
|
struct media_record **link = &output_queue.head, *previous = NULL; |
|
while (*link) { |
|
struct media_record *r = *link; |
|
if (r->data[0] <= 3 || (r->id != publisher.id && r->id != viewer.id)) { |
|
*link = r->next; if (output_queue.tail == r) output_queue.tail = previous; |
|
output_queue.bytes -= r->length; output_queue.count--; u_free(r); |
|
} else { previous = r; link = &r->next; } |
|
} |
|
} |
|
|
|
/* Все обращения к ua, включая post, защищены тем же mutex, что и detach перед destroy. */ |
|
static int media_schedule(void) { |
|
if (media_post || media_timer) return 0; |
|
if (!media_inst || !uasync_is_running(media_inst->ua)) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream schedule: core detached/stopped"); return -1; |
|
} |
|
struct posted_task *task = u_calloc(1, sizeof(*task)); |
|
if (!task) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream post allocation failed"); return -1; } |
|
task->callback = media_process; |
|
media_post = task; uasync_post_reserved(media_inst->ua, task); |
|
return 0; |
|
} |
|
|
|
/* Выдача владеет копией: lifetime scratch stream callback не выходит в Kotlin. */ |
|
static void media_output(uint64_t id, int kind, const struct stream_media_format *format, uint64_t pts, |
|
int flags, const uint8_t *data, size_t size) { |
|
if (!id || size > STREAM_MEDIA_MAX_FRAME) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android output invalid id/size=%zu", size); return; } |
|
if (output_queue.count >= RX_RECORDS || size + 20 > RX_LIMIT - output_queue.bytes) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android stream RX full id=%llu bytes=%zu count=%u", |
|
(unsigned long long)id, output_queue.bytes, output_queue.count); |
|
viewer.rx_overflow = 1; |
|
media_drop_output(); |
|
if (kind == 1 || kind == 2) { media_output(id, 3, NULL, 0, 0, NULL, 0); return; } |
|
} |
|
struct media_record *record = u_calloc(1, sizeof(*record) + size + 20); |
|
if (!record) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream RX allocation failed bytes=%zu", size); viewer.rx_overflow = 1; return; |
|
} |
|
record->id = id; record->length = size + 20; record->created = get_time_us(); |
|
uint8_t *p = record->data; 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 + 8, pts, 8); stream_write_be(p + 16, size, 4); |
|
if (size) memcpy(p + 20, data, size); |
|
queue_append(&output_queue, record); |
|
} |
|
|
|
/* Callback receiver работает под media_lock, декодирование видео/аудио выполняет внешний worker. */ |
|
static void media_frame(void *arg, const struct stream_media_format *format, |
|
const struct stream_media_video *video, const struct stream_media_audio *audio) { |
|
struct android_media_session *s = arg; |
|
if (s->rx_overflow) return; |
|
if (video) media_output(s->id, 1, format, video->pts_us, video->flags, video->data, video->length); |
|
if (audio) media_output(s->id, 2, format, audio->pts_us, 0, audio->data, audio->length); |
|
} |
|
|
|
/* Состояния потока не передаются через chat SharedFlow; здесь только владельцы выбранного viewer. */ |
|
static void media_event(struct UTUN_INSTANCE *inst, const struct stream_event *event, void *arg) { |
|
(void)arg; |
|
pthread_mutex_lock(&media_lock); |
|
if (inst == media_inst && viewer.handle && event->descriptor.group_id == viewer.group |
|
&& event->descriptor.source_node_id == viewer.source && event->descriptor.source_epoch == viewer.epoch |
|
&& !viewer.stopping && (event->type != STREAM_EVENT_CLOSED || !stream_handle_active(viewer.handle))) { |
|
if (event->type == STREAM_EVENT_DATA) { |
|
int result = stream_media_receiver_feed(viewer.receiver, event->data, event->length); |
|
if (result != STREAM_OK) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android viewer malformed/unsupported media id=%llu result=%d; closing", |
|
(unsigned long long)viewer.id, result); |
|
media_close(&viewer, 6); |
|
} else if (viewer.rx_overflow) { |
|
stream_media_receiver_gap(viewer.receiver); viewer.rx_overflow = 0; |
|
media_output(viewer.id, 3, NULL, 0, 0, NULL, 0); |
|
} |
|
} else if (event->type == STREAM_EVENT_GAP) { |
|
stream_media_receiver_gap(viewer.receiver); media_output(viewer.id, 3, NULL, 0, 0, NULL, 0); |
|
} else { |
|
viewer.reason = event->reason; |
|
media_close(&viewer, event->type == STREAM_EVENT_END ? 5 : 6); |
|
} |
|
} |
|
pthread_mutex_unlock(&media_lock); |
|
} |
|
|
|
/* Publisher держит конечное окно выдачи после FIN; abrupt detach ничего не обещает получателям. */ |
|
static void media_close(struct android_media_session *s, int kind) { |
|
if (kind) media_output(s->id, kind, NULL, 0, kind == 6 ? s->reason : 0, NULL, 0); |
|
if (s->handle) stream_close(s->handle); |
|
stream_media_receiver_destroy(s->receiver); |
|
if (s == &publisher) { queue_clear(&video_queue); queue_clear(&audio_queue); } |
|
memset(s, 0, sizeof(*s)); |
|
} |
|
|
|
/* Сформировать ограниченную пачку: аудио целиком, видео с продолжениями и несколькими началами. */ |
|
static int media_publish(void) { |
|
struct stream_media_block block = {.format = publisher.format}; |
|
uint8_t wire[7080]; size_t capacity = stream_publish_capacity(publisher.handle), used = STREAM_MEDIA_HEADER_SIZE; |
|
if (capacity > sizeof(wire) || capacity < 256) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android publisher invalid capacity=%zu", capacity); return STREAM_ERR_STATE; |
|
} |
|
struct media_record *a = audio_queue.head; |
|
while (a && block.audio_count < 8 && used + STREAM_MEDIA_AUDIO_SIZE + a->length + STREAM_MEDIA_VIDEO_SIZE < capacity) { |
|
block.audio[block.audio_count++] = (struct stream_media_audio){a->pts, (uint32_t)a->length, 960, a->data}; |
|
used += STREAM_MEDIA_AUDIO_SIZE + a->length; a = a->next; |
|
} |
|
struct media_record *v = video_queue.head; |
|
while (v && block.video_count < STREAM_MEDIA_MAX_ENTRIES && used + STREAM_MEDIA_VIDEO_SIZE < capacity) { |
|
size_t take = capacity - used - STREAM_MEDIA_VIDEO_SIZE; |
|
if (take > v->length - v->offset) take = v->length - v->offset; |
|
block.video[block.video_count++] = (struct stream_media_video){v->frame_id, v->pts, (uint32_t)v->length, |
|
(uint32_t)v->offset, (uint32_t)take, (uint8_t)v->key, v->data + v->offset}; |
|
used += STREAM_MEDIA_VIDEO_SIZE + take; |
|
if (take < v->length - v->offset) break; |
|
v = v->next; |
|
} |
|
if (!block.video_count && !block.audio_count) return STREAM_WAIT; |
|
size_t length = 0; |
|
int result = stream_media_encode(&block, wire, capacity, &length); |
|
if (result == STREAM_OK) result = stream_publish_block(publisher.handle, wire, length); |
|
if (result != STREAM_OK) return result; |
|
for (unsigned i = 0; i < block.audio_count; i++) u_free(queue_take(&audio_queue)); |
|
for (unsigned i = 0; i < block.video_count; i++) { |
|
video_queue.head->offset += block.video[i].length; |
|
if (video_queue.head->offset == video_queue.head->length) u_free(queue_take(&video_queue)); |
|
} |
|
return STREAM_OK; |
|
} |
|
|
|
/* Список содержит только проверенные публикации живых мемберов; SQL и BGP читаются в uasync. */ |
|
static void media_list_sources(void) { |
|
uint64_t group_id = list_group; list_group = 0; |
|
u_free(source_list); source_list = NULL; |
|
int result = stream_sources_json(media_inst, group_id, &source_list); |
|
cached_group = group_id; list_updated = get_time_us(); |
|
if (result < 0) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream source snapshot failed group=%llx result=%d", |
|
(unsigned long long)group_id, result); |
|
} |
|
|
|
/* Один actor обслуживает команды и не больше двух FEC-пачек за вызов; idle не будит мобильное ядро. */ |
|
static void media_process(void *arg) { |
|
(void)arg; |
|
pthread_mutex_lock(&media_lock); |
|
media_post = NULL; media_timer = NULL; |
|
if (!media_inst) { pthread_mutex_unlock(&media_lock); return; } |
|
if (watch_pending) { |
|
watch_pending = 0; |
|
if (stream_watch_sources(media_inst, watch_group) != STREAM_OK) |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream source watch failed group=%llx", (unsigned long long)watch_group); |
|
} |
|
if (list_group) media_list_sources(); |
|
struct android_media_session *sessions[] = {&publisher, &viewer}; |
|
for (unsigned i = 0; i < 2; i++) { |
|
struct android_media_session *s = sessions[i]; |
|
if (s->opening) { |
|
if (s->stopping) { media_close(s, 5); continue; } |
|
s->opening = 0; |
|
int result = STREAM_OK; |
|
s->reason = STREAM_CLOSE_FAILED; |
|
if (s->source && s->expected_epoch) { |
|
struct stream_member_state state; |
|
result = stream_member_load(media_inst, s->group, s->source, &state, NULL); |
|
if (result == STREAM_OK && (!state.published || state.source.source_epoch != s->expected_epoch)) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android selected source changed group=%llx source=%llx expected=%llx actual=%llx", |
|
(unsigned long long)s->group, (unsigned long long)s->source, (unsigned long long)s->expected_epoch, |
|
(unsigned long long)(state.published ? state.source.source_epoch : 0)); |
|
result = STREAM_ERR_CONFLICT; s->reason = STREAM_CLOSE_SOURCE_CHANGED; |
|
} |
|
} |
|
if (result == STREAM_OK) result = s->source ? stream_subscribe_open(media_inst, s->group, s->source, &s->handle) |
|
: stream_publish_open(media_inst, s->group, NULL, &s->handle); |
|
if (result == STREAM_OK && s->source) result = stream_media_receiver_create(&s->receiver, media_frame, s); |
|
struct stream_info info; |
|
if (result == STREAM_OK) { |
|
result = stream_get_info(s->handle, &info); |
|
if (result == STREAM_OK) { |
|
s->epoch = info.descriptor.source_epoch; |
|
if (s->expected_epoch && s->epoch != s->expected_epoch) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android viewer opened stale epoch id=%llu", (unsigned long long)s->id); |
|
result = STREAM_ERR_CONFLICT; s->reason = STREAM_CLOSE_SOURCE_CHANGED; |
|
} |
|
} |
|
} |
|
if (result != STREAM_OK) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream open failed id=%llu group=%llx source=%llx result=%d", |
|
(unsigned long long)s->id, (unsigned long long)s->group, (unsigned long long)s->source, result); |
|
media_close(s, 6); |
|
} else { |
|
media_output(s->id, 4, NULL, 0, 0, NULL, 0); |
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "Android stream opened id=%llu publisher=%d group=%llx source=%llx", |
|
(unsigned long long)s->id, !s->source, (unsigned long long)s->group, (unsigned long long)s->source); |
|
} |
|
} |
|
} |
|
if (viewer.stopping && viewer.id) media_close(&viewer, 5); |
|
if (publisher.handle && !stream_handle_active(publisher.handle)) media_close(&publisher, 6); |
|
uint64_t now = get_time_us(); |
|
if (video_queue.head && now - video_queue.head->created > MEDIA_AGE_US) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android TX video expired queued=%u bytes=%zu; require KEY", video_queue.count, video_queue.bytes); |
|
queue_clear(&video_queue); publisher.need_key = 1; |
|
} |
|
while (audio_queue.head && now - audio_queue.head->created > MEDIA_AGE_US) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android TX audio expired pts=%llu", (unsigned long long)audio_queue.head->pts); |
|
u_free(queue_take(&audio_queue)); |
|
} |
|
if (publisher.handle && !publisher.finished) { |
|
for (unsigned i = 0; i < 2 && (video_queue.head || audio_queue.head); i++) { |
|
size_t pending = video_queue.bytes + audio_queue.bytes - (video_queue.head ? video_queue.head->offset : 0); |
|
uint64_t oldest = video_queue.head ? video_queue.head->created : audio_queue.head->created; |
|
if (audio_queue.head && audio_queue.head->created < oldest) oldest = audio_queue.head->created; |
|
if (!publisher.stopping && pending + STREAM_MEDIA_HEADER_SIZE + STREAM_MEDIA_VIDEO_SIZE < 7080 |
|
&& now - oldest < 20000) break; |
|
int result = media_publish(); |
|
if (result < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android media publish failed result=%d", result); media_close(&publisher, 6); break; } |
|
} |
|
if (publisher.handle && publisher.stopping && !video_queue.head && !audio_queue.head) { |
|
if (stream_publish_finish(publisher.handle) != STREAM_OK) media_close(&publisher, 6); |
|
else { publisher.finished = 1; publisher.drain_until = now + 2000000; } |
|
} |
|
} |
|
if (publisher.finished && now >= publisher.drain_until) media_close(&publisher, 5); |
|
if (video_queue.head || audio_queue.head || publisher.finished) { |
|
media_timer = uasync_set_timeout(media_inst->ua, publisher.finished ? 200 : 50, NULL, media_process, "android_stream"); |
|
if (!media_timer) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android media timer allocation failed"); media_close(&publisher, 6); } |
|
} |
|
pthread_mutex_unlock(&media_lock); |
|
} |
|
|
|
/* Attach/detach — обязательная граница lifetime ядра. */ |
|
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; |
|
} |
|
media_inst = inst; stream_set_event_callback(inst, media_event, NULL); |
|
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) { |
|
if (media_post) uasync_cancel_post(inst->ua, media_post); |
|
if (media_timer) uasync_cancel_timeout(inst->ua, media_timer); |
|
media_post = NULL; media_timer = NULL; |
|
stream_set_event_callback(inst, NULL, NULL); |
|
queue_clear(&output_queue); |
|
if (publisher.id) media_close(&publisher, 0); |
|
if (viewer.id) media_close(&viewer, 0); |
|
media_inst = NULL; |
|
watch_pending = 0; watch_group = 0; |
|
u_free(source_list); source_list = NULL; list_group = cached_group = list_updated = 0; |
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "Android stream bridge detached; producers rejected"); |
|
} |
|
pthread_mutex_unlock(&media_lock); |
|
} |
|
|
|
/* Команда содержит идентификаторы, а не borrowed pointer ядра. */ |
|
int android_stream_open(uint64_t id, uint64_t group, uint64_t source, unsigned width, unsigned height, |
|
unsigned rotation, int audio, uint64_t expected_epoch) { |
|
if (!id || !group || (!source && (!width || width > 4096 || !height || height > 4096 || rotation > 270 || rotation % 90))) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream open invalid arguments"); return -1; |
|
} |
|
pthread_mutex_lock(&media_lock); |
|
struct android_media_session *s = source ? &viewer : &publisher; |
|
int result = -1; |
|
if (s->id || id == publisher.id || id == viewer.id || !media_inst || !uasync_is_running(media_inst->ua)) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream open busy/detached id=%llu", (unsigned long long)id); |
|
} else if (media_schedule() == 0) { |
|
*s = (struct android_media_session){.id = id, .group = group, .source = source, .expected_epoch = expected_epoch, |
|
.opening = 1, .need_key = 1, |
|
.format = {(uint16_t)width, (uint16_t)height, (uint16_t)rotation, STREAM_MEDIA_H264, audio ? STREAM_MEDIA_OPUS : 0}}; |
|
result = 0; |
|
} |
|
pthread_mutex_unlock(&media_lock); return result; |
|
} |
|
|
|
/* Последняя выбранная группа заменяет ещё не обработанную команду, указатели ядра не покидают mutex. */ |
|
int android_stream_watch(uint64_t group) { |
|
pthread_mutex_lock(&media_lock); |
|
int result = media_inst && uasync_is_running(media_inst->ua) ? media_schedule() : -1; |
|
if (result == 0) { watch_group = group; watch_pending = 1; } |
|
else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android stream watch rejected group=%llx", (unsigned long long)group); |
|
pthread_mutex_unlock(&media_lock); return result; |
|
} |
|
|
|
/* Перегрузка удаляет целые видео-кадры и блокирует P-кадры до нового KEY. */ |
|
int android_stream_feed(uint64_t id, int video, uint64_t pts, int key, const uint8_t *data, size_t length) { |
|
if (!data || !length || length > (video ? STREAM_MEDIA_MAX_FRAME : STREAM_MEDIA_MAX_OPUS) || pts > INT64_MAX) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream feed invalid length=%zu", length); return -1; |
|
} |
|
pthread_mutex_lock(&media_lock); |
|
int result = -1, track = video ? 0 : 1; |
|
if (!media_inst || publisher.id != id || publisher.stopping || publisher.finished) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android stream feed rejected id=%llu owner=%llu attached=%d stopping=%d finished=%d", |
|
(unsigned long long)id, (unsigned long long)publisher.id, media_inst != NULL, publisher.stopping, publisher.finished); |
|
goto done; |
|
} |
|
if ((!video && !publisher.format.audio_codec) || (publisher.seen[track] && pts <= publisher.last_pts[track])) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream feed invalid track/PTS id=%llu pts=%llu", |
|
(unsigned long long)id, (unsigned long long)pts); goto done; |
|
} |
|
if (video && (video_queue.bytes + audio_queue.bytes + length > TX_LIMIT |
|
|| video_queue.count + audio_queue.count >= TX_RECORDS)) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android TX overload bytes=%zu count=%u; require KEY", |
|
video_queue.bytes + audio_queue.bytes, video_queue.count + audio_queue.count); |
|
queue_clear(&video_queue); publisher.need_key = 1; |
|
} |
|
if (video && publisher.need_key && !key) { result = 0; goto done; } |
|
if (video_queue.bytes + audio_queue.bytes + length > TX_LIMIT || video_queue.count + audio_queue.count >= TX_RECORDS) { |
|
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android TX admission refused length=%zu video=%d", length, video); result = 0; goto done; |
|
} |
|
struct media_record *record = u_calloc(1, sizeof(*record) + length); |
|
if (!record) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android TX allocation failed bytes=%zu", length); goto done; } |
|
if (media_schedule() < 0) { u_free(record); goto done; } |
|
record->length = length; record->pts = pts; record->created = get_time_us(); record->key = key ? STREAM_MEDIA_KEY : 0; |
|
if (video) { record->frame_id = ++publisher.next_frame; publisher.need_key = 0; } |
|
memcpy(record->data, data, length); queue_append(video ? &video_queue : &audio_queue, record); |
|
publisher.seen[track] = 1; publisher.last_pts[track] = pts; result = 1; |
|
done: |
|
pthread_mutex_unlock(&media_lock); return result; |
|
} |
|
|
|
/* Stop идемпотентен; publisher дренирует принятую очередь, viewer закрывается сразу в actor. */ |
|
void android_stream_stop(uint64_t id) { |
|
pthread_mutex_lock(&media_lock); |
|
struct android_media_session *s = publisher.id == id ? &publisher : viewer.id == id ? &viewer : NULL; |
|
if (s) { s->stopping = 1; if (media_schedule() < 0) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream stop schedule failed"); } |
|
pthread_mutex_unlock(&media_lock); |
|
} |
|
|
|
/* Выбрать только события текущего owner; старые owner-события имеют ограниченный TTL. */ |
|
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); |
|
struct media_record **link = &output_queue.head, *previous = NULL; |
|
uint8_t *data = NULL; uint64_t now = get_time_us(); |
|
while (*link) { |
|
struct media_record *r = *link; |
|
if (r->id == id || now - r->created > 5000000) { |
|
*link = r->next; if (output_queue.tail == r) output_queue.tail = previous; |
|
output_queue.bytes -= r->length; output_queue.count--; |
|
if (r->id == id) { |
|
data = u_malloc((uint32_t)r->length); |
|
if (data) { memcpy(data, r->data, r->length); *length = r->length; } |
|
else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream pull allocation failed"); |
|
u_free(r); break; |
|
} |
|
u_free(r); |
|
} else { previous = r; link = &r->next; } |
|
} |
|
pthread_mutex_unlock(&media_lock); return data; |
|
} |
|
|
|
/* Внешний поток не читает ни borrowed instance, ни SQLite; snapshot строится следующим actor-вызовом. */ |
|
char *android_stream_sources(uint64_t group) { |
|
pthread_mutex_lock(&media_lock); |
|
if (group && media_inst && uasync_is_running(media_inst->ua) |
|
&& (group != cached_group || get_time_us() - list_updated >= 500000)) { |
|
list_group = group; |
|
if (media_schedule() < 0) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android source snapshot schedule failed"); |
|
} |
|
char *copy = u_strdup(group == cached_group && source_list ? source_list : "[]"); |
|
if (!copy) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android source snapshot copy allocation failed"); |
|
pthread_mutex_unlock(&media_lock); return copy; |
|
}
|
|
|