diff --git a/tools/chatgui-android/libutun_lite/stream_bridge.c b/tools/chatgui-android/libutun_lite/stream_bridge.c index 0b5080d9..86f4385f 100644 --- a/tools/chatgui-android/libutun_lite/stream_bridge.c +++ b/tools/chatgui-android/libutun_lite/stream_bridge.c @@ -10,13 +10,11 @@ #include "../../../src/stream/stream_member.h" #include "../../../src/stream/stream_media.h" #include "../../../src/stream/stream_network.h" +#include "../../../src/stream/stream_publisher.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; @@ -39,11 +37,9 @@ 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 void *stats_timer; -static struct android_stream_tx_stats tx_stats; -static uint64_t stats_report_us; +static struct stream_publisher *transmitter; static struct android_media_session publisher, viewer; -static struct media_queue video_queue, audio_queue, output_queue; +static struct media_queue output_queue; static struct stream_media_status viewer_status; static uint64_t viewer_status_id; static uint64_t watch_group; @@ -51,38 +47,13 @@ static int watch_pending; static void media_process(void *arg); static void media_close(struct android_media_session *s, int kind); -static void media_stats(void *arg) { - (void)arg; pthread_mutex_lock(&media_lock); stats_timer = NULL; - if (media_inst && publisher.handle && stream_handle_active(publisher.handle)) { - uint64_t now = get_time_us(); struct etcp_tx_snapshot transport; struct stream_info info; - if (etcp_tx_get_snapshot(media_inst, now, &transport) == 0 && stream_get_info(publisher.handle, &info) == STREAM_OK) { - tx_stats = (struct android_stream_tx_stats){.id = publisher.id, .sample_us = now, - .video_bytes = video_queue.bytes - (video_queue.head ? video_queue.head->offset : 0), - .audio_bytes = audio_queue.bytes, .stream_bytes = stream_network_pending_bytes(media_inst, &info.descriptor), - .transport_bytes = transport.queued_bytes, .inflight_bytes = transport.inflight_bytes, - .acked_1s = transport.acked_1s, .connections = transport.connections}; - tx_stats.total_bytes = tx_stats.video_bytes + tx_stats.audio_bytes + tx_stats.stream_bytes - + tx_stats.transport_bytes + tx_stats.inflight_bytes; - if (now - stats_report_us >= 1000000) { - stats_report_us = now; - DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "TX id=%llu video=%llu audio=%llu RS=%llu ETCP=%llu inflight=%llu total=%llu ACK_1s=%llu conns=%u", - (unsigned long long)tx_stats.id, (unsigned long long)tx_stats.video_bytes, (unsigned long long)tx_stats.audio_bytes, - (unsigned long long)tx_stats.stream_bytes, (unsigned long long)tx_stats.transport_bytes, - (unsigned long long)tx_stats.inflight_bytes, (unsigned long long)tx_stats.total_bytes, - (unsigned long long)tx_stats.acked_1s, tx_stats.connections); - } - } - stats_timer = uasync_set_timeout(media_inst->ua, 1000, NULL, media_stats, "stream_tx_stats"); - if (!stats_timer) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "TX stats timer allocation failed"); - } - pthread_mutex_unlock(&media_lock); -} - 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); - int result = media_inst && publisher.id == id && tx_stats.id == id ? 0 : -1; - if (!result) *out = tx_stats; + 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; } @@ -187,49 +158,9 @@ static void media_event(struct UTUN_INSTANCE *inst, const struct stream_event *e 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); - if (s == &publisher) { - if (stats_timer) { uasync_cancel_timeout(media_inst->ua, stats_timer); stats_timer = NULL; } - memset(&tx_stats, 0, sizeof(tx_stats)); 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[STREAM_DEFAULT_INPUT_CAPACITY]; - 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; -} - /* Один actor обслуживает команды и не больше двух FEC-пачек за вызов; idle не будит мобильное ядро. */ static void media_process(void *arg) { (void)arg; @@ -241,8 +172,8 @@ static void media_process(void *arg) { 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); } - struct android_media_session *sessions[] = {&publisher, &viewer}; - for (unsigned i = 0; i < 2; i++) { + struct android_media_session *sessions[] = {&viewer}; + for (unsigned i = 0; i < 1; i++) { struct android_media_session *s = sessions[i]; if (s->opening) { if (s->stopping) { media_close(s, 5); continue; } @@ -280,10 +211,6 @@ static void media_process(void *arg) { media_close(s, 6); } else { if (s->source) { viewer_status_id = s->id; memset(&viewer_status, 0, sizeof(viewer_status)); s->want_read = 1; } - else { - stats_timer = uasync_set_timeout(media_inst->ua, 1000, NULL, media_stats, "stream_tx_stats"); - if (!stats_timer) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "TX stats start timer allocation failed"); - } 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); @@ -321,36 +248,6 @@ static void media_process(void *arg) { memset(&viewer_status, 0, sizeof(viewer_status)); } else { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android playback read failed result=%d", result); media_close(&viewer, 6); } } - 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 < stream_publish_capacity(publisher.handle) - && 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 (!media_post && (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); } @@ -361,6 +258,9 @@ int android_stream_attach(struct UTUN_INSTANCE *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 (!transmitter && stream_publisher_create(&transmitter, inst) < 0) { + 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); @@ -375,7 +275,7 @@ void android_stream_detach(struct UTUN_INSTANCE *inst) { stream_watch_sources(inst, 0); stream_set_event_callback(inst, NULL, NULL); queue_clear(&output_queue); - if (publisher.id) media_close(&publisher, 0); + stream_publisher_destroy(transmitter); transmitter = NULL; memset(&publisher, 0, sizeof(publisher)); if (viewer.id) media_close(&viewer, 0); media_inst = NULL; watch_pending = 0; watch_group = 0; @@ -399,12 +299,13 @@ int android_stream_open(uint64_t id, uint64_t group, uint64_t source, unsigned w if (codec < STREAM_MEDIA_H264 || codec > STREAM_MEDIA_H266 || 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) { + } else if ((!source || media_schedule() == 0)) { *s = (struct android_media_session){.id = id, .group = group, .source = source, .expected_epoch = expected_epoch, .opening = 1, .need_key = 1, .listen_audio = !!audio, .position = -1, .video_ahead = STREAM_MEDIA_READ_AHEAD_US, .audio_ahead = STREAM_MEDIA_READ_AHEAD_US, .format = {(uint16_t)width, (uint16_t)height, (uint16_t)rotation, (uint8_t)codec, audio ? STREAM_MEDIA_OPUS_STEREO : 0, 1}}; - result = 0; + result = source ? 0 : stream_publisher_open(transmitter, id, group, &s->format); + if (result < 0) memset(s, 0, sizeof(*s)); } pthread_mutex_unlock(&media_lock); return result; } @@ -421,64 +322,8 @@ int android_stream_watch(uint64_t group) { /* Перегрузка удаляет целые видео-кадры и блокирует P-кадры до нового KEY. */ 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) { - 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; - } - int reconfigure = 0; - if (video) { - if (!format || !format->revision || !format->width || format->width > 4096 || !format->height || format->height > 4096 - || format->rotation > 270 || format->rotation % 90 - || format->video_codec < STREAM_MEDIA_H264 || format->video_codec > STREAM_MEDIA_H266 - || format->audio_codec != publisher.format.audio_codec) { - DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android video feed invalid configuration id=%llu", (unsigned long long)id); goto done; - } - if (format->revision < publisher.format.revision) { - DEBUG_WARN(DEBUG_CATEGORY_STREAM, "Android stale video revision=%u current=%u", format->revision, publisher.format.revision); - result = 0; goto done; - } - reconfigure = format->revision > publisher.format.revision; - if (reconfigure && !key) { DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "Android new revision=%u waits KEY", format->revision); result = 0; goto done; } - if (!reconfigure && (format->video_codec != publisher.format.video_codec || format->width != publisher.format.width || format->height != publisher.format.height - || format->rotation != publisher.format.rotation)) { - DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android video configuration changed without revision=%u", format->revision); 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 && !reconfigure && (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 ((reconfigure ? 0 : video_queue.bytes) + audio_queue.bytes + length > TX_LIMIT - || (reconfigure ? 0 : 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; } - if (reconfigure) { - queue_clear(&video_queue); - DEBUG_INFO(DEBUG_CATEGORY_STREAM, "Android video configuration committed id=%llu revision=%u->%u dimensions=%ux%u rotation=%u", - (unsigned long long)id, publisher.format.revision, format->revision, format->width, format->height, format->rotation); - publisher.format = *format; - } - 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: + int result = stream_publisher_feed(transmitter, id, video, pts, key, data, length, format); pthread_mutex_unlock(&media_lock); return result; } @@ -486,7 +331,8 @@ done: 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"); } + if (s == &publisher) { s->stopping = 1; stream_publisher_stop(transmitter, id); } + else if (s) { s->stopping = 1; if (media_schedule() < 0) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream stop schedule failed"); } pthread_mutex_unlock(&media_lock); } @@ -494,6 +340,18 @@ void android_stream_stop(uint64_t id) { 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); + if (publisher.id) { + struct stream_publisher_status status; + if (stream_publisher_snapshot(transmitter, publisher.id, &status) == 0) { + if (publisher.opening && status.state >= STREAM_PUBLISHER_LIVE && status.state <= STREAM_PUBLISHER_DRAINING) { + publisher.opening = 0; publisher.epoch = status.epoch; media_output(publisher.id, 4, NULL, 0, 0, NULL, 0); + } + if (status.state == STREAM_PUBLISHER_ENDED || status.state == STREAM_PUBLISHER_FAILED) { + media_output(publisher.id, status.state == STREAM_PUBLISHER_ENDED ? 5 : 6, NULL, 0, status.reason, NULL, 0); + memset(&publisher, 0, sizeof(publisher)); + } + } + } struct media_record **link = &output_queue.head, *previous = NULL; uint8_t *data = NULL; uint64_t now = get_time_us(); while (*link) { diff --git a/tools/chatgui/transport/stream_bridge.cpp b/tools/chatgui/transport/stream_bridge.cpp index 356b42a5..2b426ddf 100644 --- a/tools/chatgui/transport/stream_bridge.cpp +++ b/tools/chatgui/transport/stream_bridge.cpp @@ -26,6 +26,7 @@ struct Bridge { uint64_t statusId = 0; Viewer viewer, requested; STREAM_HANDLE* handle = nullptr; + stream_publisher* publisher = nullptr; std::deque output; size_t bytes = 0; } bridge; @@ -185,6 +186,7 @@ bool gui_stream_attach(UTUN_INSTANCE* inst) { if (!inst || !inst->stream || bridge.inst) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream attach rejected"); return false; } + if (stream_publisher_create(&bridge.publisher, inst) < 0) return false; bridge.inst = inst; bridge.output.clear(); bridge.bytes = 0; stream_set_event_callback(inst, mediaEvent, nullptr); if (bridge.watchPending) { @@ -203,6 +205,7 @@ void gui_stream_detach(UTUN_INSTANCE* inst) { bridge.pending = nullptr; stream_set_event_callback(inst, nullptr, nullptr); if (bridge.viewer.id) closeViewer(StreamRecordKind::Closed, STREAM_CLOSE_SERVICE); if (bridge.opening) output({bridge.requested.id, 0, StreamRecordKind::Closed, {}, 0, STREAM_CLOSE_SERVICE}); + stream_publisher_destroy(bridge.publisher); bridge.publisher = nullptr; bridge.inst = nullptr; bridge.requested = {}; bridge.opening = bridge.stopping = bridge.watchPending = false; DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream bridge detached"); } @@ -265,3 +268,21 @@ bool gui_stream_resync(uint64_t id) { } bool gui_stream_ready() { std::lock_guard guard(bridge.lock); return bridge.inst != nullptr; } + +bool gui_stream_publish_open(uint64_t id, uint64_t group, const stream_media_format& format) { + std::lock_guard guard(bridge.lock); + return stream_publisher_open(bridge.publisher, id, group, &format) == 0; +} +int gui_stream_publish_feed(uint64_t id, bool video, uint64_t pts, bool key, const QByteArray& data, + const stream_media_format* format) { + std::lock_guard guard(bridge.lock); + return stream_publisher_feed(bridge.publisher, id, video, pts, key, + reinterpret_cast(data.constData()), static_cast(data.size()), format); +} +void gui_stream_publish_stop(uint64_t id) { + std::lock_guard guard(bridge.lock); stream_publisher_stop(bridge.publisher, id); +} +bool gui_stream_publish_status(uint64_t id, stream_publisher_status& status) { + std::lock_guard guard(bridge.lock); + return stream_publisher_snapshot(bridge.publisher, id, &status) == 0; +} diff --git a/tools/chatgui/transport/stream_bridge.h b/tools/chatgui/transport/stream_bridge.h index 69bd1115..5bec4d30 100644 --- a/tools/chatgui/transport/stream_bridge.h +++ b/tools/chatgui/transport/stream_bridge.h @@ -8,6 +8,7 @@ #include #include "../../../src/stream/stream_media.h" #include "../../../src/stream/stream_media_buffer.h" +#include "../../../src/stream/stream_publisher.h" struct UTUN_INSTANCE; enum class StreamRecordKind { Video, Audio, Gap, Opened, End, Closed }; @@ -29,3 +30,9 @@ bool gui_stream_pull(uint64_t id, StreamRecord& record, int64_t position = -1, bool gui_stream_status(uint64_t id, stream_media_status& status); bool gui_stream_resync(uint64_t id); bool gui_stream_ready(); +/* Publisher независим от viewer; lifetime ядра ограничивает общий bridge mutex. */ +bool gui_stream_publish_open(uint64_t id, uint64_t group, const stream_media_format& format); +int gui_stream_publish_feed(uint64_t id, bool video, uint64_t pts, bool key, const QByteArray& data, + const stream_media_format* format = nullptr); +void gui_stream_publish_stop(uint64_t id); +bool gui_stream_publish_status(uint64_t id, stream_publisher_status& status);