From 6bf27b6c4e76a99e9b79e61981dc7e935f30f1a5 Mon Sep 17 00:00:00 2001 From: hackerman-kl Date: Sat, 25 Apr 2026 10:43:44 +0200 Subject: [PATCH] milan-avb: cmd-get-set-stream-info: add command handler --- .../cmd-get-set-stream-info.c | 453 ++++++++++++++++++ .../cmd-get-set-stream-info.h | 31 ++ 2 files changed, 484 insertions(+) create mode 100644 src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.c create mode 100644 src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.h diff --git a/src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.c b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.c new file mode 100644 index 000000000..0cd5ac68d --- /dev/null +++ b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.c @@ -0,0 +1,453 @@ +/* SPDX-FileCopyrightText: Copyright © 2025 Alexandre Malki */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include "../aecp.h" +#include "../aecp-aem.h" +#include "../aecp-aem-descriptors.h" +#include "../aecp-aem-state.h" +#include "../maap.h" +#include "../mrp.h" + +#include "cmd-get-set-stream-info.h" +#include "cmd-resp-helpers.h" +#include "reply-unsol-helpers.h" + + +/* Milan Section 5.4.2.10.2.1 / Table 5.12 — flags_ex.REGISTERING for Stream Output + * requires the entity to be DECLARING the Talker attribute. The MRP + * applicant is "declaring" in any non-observer state (anything other than + * VO/AO/QO/LO). Returns false safely if mrp is NULL, which happens before + * stream_activate runs (no foreign listener has probed yet, so we're not + * declaring anything). */ +static inline bool mrp_is_declaring(struct avb_mrp_attribute *mrp) +{ + uint8_t state; + if (mrp == NULL) + return false; + state = avb_mrp_attribute_get_applicant_state(mrp); + return state != AVB_MRP_VO && state != AVB_MRP_AO && + state != AVB_MRP_QO && state != AVB_MRP_LO; +} + +/* Full wire size of a Milan GET_STREAM_INFO response (Figure 5.1): + * 14 (Ethernet) + 4 (AVTP common hdr) + 20 (target+controller+seq+cmd) + + * 56 (Milan-shaped setget_stream_info body) = 94 bytes. */ +#define AVB_AECP_GET_STREAM_INFO_RESPONSE_LEN \ + (int)(sizeof(struct avb_ethernet_header) + \ + sizeof(struct avb_packet_aecp_aem) + \ + sizeof(struct avb_packet_aecp_aem_setget_stream_info)) + +#define AVB_AECP_GET_STREAM_INFO_CDL \ + (uint16_t)(AVB_AECP_GET_STREAM_INFO_RESPONSE_LEN - \ + sizeof(struct avb_ethernet_header) - \ + sizeof(struct avb_packet_header)) + +/* Hive (and 1722.1 controllers in general) sends GET_STREAM_INFO as a short + * command — only the descriptor pair, sometimes plus the flags word. The + * Milan response has to be the full 94-byte frame regardless. Initialise the + * extra trailing bytes and patch the AVTP control_data_length so the packet + * is well-formed on the wire. */ +static void prepare_full_response(uint8_t *buf, int in_len) +{ + struct avb_packet_aecp_header *aecp_hdr; + int total = AVB_AECP_GET_STREAM_INFO_RESPONSE_LEN; + + if (in_len < total) + memset(buf + in_len, 0, total - in_len); + + aecp_hdr = SPA_PTROFF(buf, sizeof(struct avb_ethernet_header), void); + AVB_PACKET_SET_LENGTH(&aecp_hdr->hdr, AVB_AECP_GET_STREAM_INFO_CDL); +} + +static int build_stream_info_response(struct aecp *aecp, const void *m, int len, + struct avb_packet_aecp_aem_setget_stream_info *reply, + uint32_t flags_host, uint32_t flags_ex_host, + uint8_t pbsta, uint8_t acmpsta, + uint64_t stream_format_be, uint64_t stream_id_be, + uint32_t msrp_acc_lat_host, const uint8_t dest_mac[6], + uint8_t msrp_fail_code, uint64_t msrp_fail_bridge_id_be, + uint16_t stream_vlan_id_host) +{ + (void)len; + (void)aecp; + (void)m; + + reply->flags.flags = htonl(flags_host); + reply->stream_format = stream_format_be; + reply->stream_id = stream_id_be; + reply->msrp_accumulated_latency = htonl(msrp_acc_lat_host); + memcpy(reply->stream_dest_mac, dest_mac, 6); + reply->msrp_failure_code = msrp_fail_code; + reply->reserved = 0; + reply->msrp_failure_bridge_id = msrp_fail_bridge_id_be; + reply->stream_vlan_id = htons(stream_vlan_id_host); + reply->stream_vlan_id_reserved = 0; + reply->flags_ex.milan_v12.flags_ex = htonl(flags_ex_host); + reply->flags_ex.milan_v12.pbsta_acmpsta = + htonl(AVB_AEM_STREAM_INFO_PBSTA_ACMPSTA(pbsta, acmpsta)); + return 0; +} + +static void populate_input_response(struct aecp *aecp, + struct aecp_aem_stream_input_state_milan_v12 *si, + struct avb_packet_aecp_aem_setget_stream_info *reply) +{ + struct stream_common *sc = &si->stream_in_sta.common; + struct avb_msrp_attribute *lattr = &sc->lstream_attr; + struct avb_msrp_attribute *taattr = &sc->tastream_attr; + struct avb_msrp_attribute *tfattr = &sc->tfstream_attr; + uint32_t flags_host = 0; + uint32_t flags_ex_host = 0; + uint64_t stream_format_be; + uint64_t stream_id_be = 0; + uint8_t dest_mac[6] = {0}; + uint16_t vlan_id_host = 0; + uint32_t msrp_acc_lat = 0; + uint8_t msrp_fail_code = 0; + uint64_t msrp_fail_bridge_be = 0; + bool bound, settled, ta_observed, tf_observed, registering; + (void)aecp; + + stream_format_be = si->stream_in_sta.desc.current_format; + + /* Milan Section 5.3.8.2: bound iff the listener has a saved talker stream id. */ + bound = (lattr->attr.listener.stream_id != 0); + + /* Milan Section 5.3.8.5: settled iff probing has completed. */ + settled = (si->acmp_sta.probing_status == 3); + (void)settled; + + /* Milan Section 5.3.8.8 / Table 5.10 REGISTERING bit: Talker Advertise OR + * Talker Failed matching the saved SRP params is currently registered. + * tastream_attr / tfstream_attr are the foreign declarations we observe; + * registrar IN means we've heard them on the wire. + * + * For listeners, only lstream_attr and tfstream_attr are created in + * server_create_stream — tastream_attr.mrp is NULL until/unless we + * later wire in talker-advertise observation. Guard the deref. */ + ta_observed = (taattr->mrp != NULL) && + (avb_mrp_attribute_get_registrar_state(taattr->mrp) == AVB_MRP_IN); + tf_observed = (tfattr->mrp != NULL) && + (avb_mrp_attribute_get_registrar_state(tfattr->mrp) == AVB_MRP_IN); + registering = ta_observed || tf_observed; + + /* Stream Input — expose the identity fields unconditionally so a + * controller (Hive) can show them even before BIND_RX or before SRP + * converges. The actual BOUND state is signalled separately via the + * CONNECTED flag and FAST_CONNECT/SAVED_STATE. Values default to 0 + * when not yet known, which is a valid representation per Milan + * Section 5.4.2.10.1.1 (the *_VALID flag indicates the field is meaningful, + * not that it is non-zero). */ + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_FORMAT_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_ID_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_DEST_MAC_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_VLAN_ID_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID; + + stream_id_be = lattr->attr.listener.stream_id; + memcpy(dest_mac, sc->stream.addr, 6); + vlan_id_host = sc->stream.vlan_id; + + if (bound) { + flags_host |= AVB_AEM_STREAM_INFO_FLAG_FAST_CONNECT; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_SAVED_STATE; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_CONNECTED; + flags_host |= si->stream_in_sta.started ? 0 + : AVB_AEM_STREAM_INFO_FLAG_STREAMING_WAIT; + } + + /* Section 5.4.2.10.1: msrp_accumulated_latency comes from the registered + * Talker attribute. Prefer Talker Advertise (the success path); fall + * back to Talker Failed if only that's been observed. 0 otherwise. */ + if (ta_observed) + msrp_acc_lat = ntohl(taattr->attr.talker.accumulated_latency); + else if (tf_observed) + msrp_acc_lat = ntohl(tfattr->attr.talker_fail.talker.accumulated_latency); + + if (tf_observed) { + flags_host |= AVB_AEM_STREAM_INFO_FLAG_SRP_REGISTERING_FAILED; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_MSRP_FAILURE_VALID; + msrp_fail_code = tfattr->attr.talker_fail.failure_code; + msrp_fail_bridge_be = tfattr->attr.talker_fail.bridge_id; + } + + /* Table 5.10 flags_ex: REGISTERING (bit 31) iff Talker Advertise/Failed + * matching SRP params is registered. */ + if (registering) + flags_ex_host |= AVB_AEM_STREAM_INFO_FLAGS_EX_REGISTERING; + + build_stream_info_response(aecp, NULL, 0, reply, + flags_host, flags_ex_host, + si->acmp_sta.probing_status, si->acmp_sta.acmp_status, + stream_format_be, stream_id_be, msrp_acc_lat, dest_mac, + msrp_fail_code, msrp_fail_bridge_be, vlan_id_host); + + pw_log_debug("populate STREAM_INPUT stream_format=0x%016" PRIx64 + " flags=0x%08x flags_ex=0x%08x pbsta=%u acmpsta=%u " + "stream_id=0x%016" PRIx64, + be64toh(stream_format_be), + flags_host, flags_ex_host, + si->acmp_sta.probing_status, si->acmp_sta.acmp_status, + be64toh(stream_id_be)); +} + +static int handle_get_stream_input(struct aecp *aecp, const void *m, int len, + struct aecp_aem_stream_input_state_milan_v12 *si) +{ + uint8_t buf[2048]; + struct avb_ethernet_header *h_reply; + struct avb_packet_aecp_aem *p_reply; + struct avb_packet_aecp_aem_setget_stream_info *reply; + + memcpy(buf, m, len); + prepare_full_response(buf, len); + h_reply = (struct avb_ethernet_header *)buf; + p_reply = SPA_PTROFF(h_reply, sizeof(*h_reply), void); + reply = (struct avb_packet_aecp_aem_setget_stream_info *)p_reply->payload; + + populate_input_response(aecp, si, reply); + + return reply_success(aecp, buf, AVB_AECP_GET_STREAM_INFO_RESPONSE_LEN); +} + +static void populate_output_response(struct aecp *aecp, uint16_t desc_index, + struct aecp_aem_stream_output_state *so, + struct avb_packet_aecp_aem_setget_stream_info *reply) +{ + struct stream_common *sc = &so->common; + struct avb_msrp_attribute *taattr = &sc->tastream_attr; + const uint8_t zero_mac[6] = {0}; + uint8_t taa_reg, lst_reg; + uint32_t flags_host = 0; + uint32_t flags_ex_host = 0; + uint64_t stream_id_be = 0; + uint8_t dest_mac[6] = {0}; + uint16_t vlan_id_host = 0; + bool talker_declaring; + bool reg_failed_listener; + + taa_reg = avb_mrp_attribute_get_registrar_state(taattr->mrp); + lst_reg = avb_mrp_attribute_get_registrar_state(sc->lstream_attr.mrp); + (void)taa_reg; + (void)lst_reg; + + /* Milan Section 5.4.2.10.2.1 / Table 5.12 — REGISTERING for Stream Output + * means: declaring TA or TF AND a matching Listener attribute is + * registered. We only declare TA (no TALKER_FAILED), so check + * tastream_attr's MRP applicant state. The underlying *_VALID flags + * for stream_id/dest_mac/vlan_id remain always 1 per Table 5.11 — the + * values are static config known from boot, independent of whether + * we have begun declaring on the wire. */ + talker_declaring = mrp_is_declaring(taattr->mrp); + reg_failed_listener = false; + + /* Table 5.11 — Stream Output flags. STREAM_FORMAT_VALID, the three + * stream_*_VALID flags, MSRP_ACC_LAT_VALID, and CONNECTED (BOUND) are + * always 1 per Milan Section 5.4.2.10.2. */ + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_FORMAT_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_CONNECTED; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_ID_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_DEST_MAC_VALID; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_STREAM_VLAN_ID_VALID; + + if (memcmp(sc->stream.addr, zero_mac, 6) == 0) + (void)avb_maap_get_address(aecp->server->maap, + sc->stream.addr, desc_index); + + stream_id_be = htobe64(sc->stream.id); + memcpy(dest_mac, sc->stream.addr, 6); + vlan_id_host = sc->stream.vlan_id; + + if (reg_failed_listener) { + flags_host |= AVB_AEM_STREAM_INFO_FLAG_SRP_REGISTERING_FAILED; + flags_host |= AVB_AEM_STREAM_INFO_FLAG_MSRP_FAILURE_VALID; + } + + /* flags_ex.REGISTERING per Table 5.12: declaring TA/TF AND a matching + * Listener attribute is registered. */ + if (talker_declaring && so->listener_observed) + flags_ex_host |= AVB_AEM_STREAM_INFO_FLAGS_EX_REGISTERING; + + /* Section 5.4.2.10.2: pbsta and acmpsta shall be 0 for Stream Output. */ + build_stream_info_response(aecp, NULL, 0, reply, + flags_host, flags_ex_host, + 0, 0, + so->desc.current_format, stream_id_be, + so->presentation_time_offset_ns, dest_mac, + 0, 0, vlan_id_host); + + pw_log_debug("populate STREAM_OUTPUT stream_format=0x%016" PRIx64 + " flags=0x%08x flags_ex=0x%08x pres_time_off=%u " + "stream_id=0x%016" PRIx64, + be64toh(so->desc.current_format), + flags_host, flags_ex_host, + so->presentation_time_offset_ns, + be64toh(stream_id_be)); +} + +static int handle_get_stream_output(struct aecp *aecp, const void *m, int len, + uint16_t desc_index, struct aecp_aem_stream_output_state *so) +{ + uint8_t buf[2048]; + struct avb_ethernet_header *h_reply; + struct avb_packet_aecp_aem *p_reply; + struct avb_packet_aecp_aem_setget_stream_info *reply; + + memcpy(buf, m, len); + prepare_full_response(buf, len); + h_reply = (struct avb_ethernet_header *)buf; + p_reply = SPA_PTROFF(h_reply, sizeof(*h_reply), void); + reply = (struct avb_packet_aecp_aem_setget_stream_info *)p_reply->payload; + + populate_output_response(aecp, desc_index, so, reply); + + return reply_success(aecp, buf, AVB_AECP_GET_STREAM_INFO_RESPONSE_LEN); +} + +int handle_cmd_get_stream_info_milan_v12(struct aecp *aecp, int64_t now, + const void *m, int len) +{ + const struct avb_ethernet_header *h = m; + const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); + const struct avb_packet_aecp_aem_setget_stream_info *cmd = + (const struct avb_packet_aecp_aem_setget_stream_info *)p->payload; + uint16_t desc_type = ntohs(cmd->descriptor_type); + uint16_t desc_index = ntohs(cmd->descriptor_index); + struct descriptor *desc; + + (void)now; + + desc = server_find_descriptor(aecp->server, desc_type, desc_index); + if (desc == NULL) + return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len); + + if (desc_type == AVB_AEM_DESC_STREAM_INPUT) + return handle_get_stream_input(aecp, m, len, + (struct aecp_aem_stream_input_state_milan_v12 *)desc->ptr); + if (desc_type == AVB_AEM_DESC_STREAM_OUTPUT) + return handle_get_stream_output(aecp, m, len, desc_index, + (struct aecp_aem_stream_output_state *)desc->ptr); + + return reply_status(aecp, AVB_AECP_AEM_STATUS_BAD_ARGUMENTS, m, len); +} + +int handle_cmd_set_stream_info_milan_v12(struct aecp *aecp, int64_t now, + const void *m, int len) +{ + const struct avb_ethernet_header *h = m; + const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); + const struct avb_packet_aecp_aem_setget_stream_info *cmd = + (const struct avb_packet_aecp_aem_setget_stream_info *)p->payload; + uint16_t desc_type = ntohs(cmd->descriptor_type); + uint16_t desc_index = ntohs(cmd->descriptor_index); + uint32_t flags_host = ntohl(cmd->flags.flags); + struct descriptor *desc; + struct aecp_aem_stream_output_state *so; + + (void)now; + + /* Milan Section 5.4.2.9: SET_STREAM_INFO is not implemented for Stream Inputs. */ + if (desc_type == AVB_AEM_DESC_STREAM_INPUT) + return reply_not_supported(aecp, m, len); + if (desc_type != AVB_AEM_DESC_STREAM_OUTPUT) + return reply_status(aecp, AVB_AECP_AEM_STATUS_BAD_ARGUMENTS, m, len); + + desc = server_find_descriptor(aecp->server, desc_type, desc_index); + if (desc == NULL) + return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len); + so = desc->ptr; + + /* Section 5.4.2.9: any unsupported sub-command → NOT_SUPPORTED for the whole + * command. We support only MSRP_ACC_LAT_VALID. */ + { + uint32_t supported_mask = AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID; + uint32_t valid_mask = + AVB_AEM_STREAM_INFO_FLAG_CLASS_B | + AVB_AEM_STREAM_INFO_FLAG_FAST_CONNECT | + AVB_AEM_STREAM_INFO_FLAG_SAVED_STATE | + AVB_AEM_STREAM_INFO_FLAG_STREAMING_WAIT | + AVB_AEM_STREAM_INFO_FLAG_SUPPORTS_ENCRYPTED | + AVB_AEM_STREAM_INFO_FLAG_ENCRYPTED_PDU | + AVB_AEM_STREAM_INFO_FLAG_SRP_REGISTERING_FAILED | + AVB_AEM_STREAM_INFO_FLAG_CL_ENTRIES_VALID | + AVB_AEM_STREAM_INFO_FLAG_NO_SRP | + AVB_AEM_STREAM_INFO_FLAG_UDP | + AVB_AEM_STREAM_INFO_FLAG_STREAM_VLAN_ID_VALID | + AVB_AEM_STREAM_INFO_FLAG_CONNECTED | + AVB_AEM_STREAM_INFO_FLAG_MSRP_FAILURE_VALID | + AVB_AEM_STREAM_INFO_FLAG_STREAM_DEST_MAC_VALID | + AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID | + AVB_AEM_STREAM_INFO_FLAG_STREAM_ID_VALID | + AVB_AEM_STREAM_INFO_FLAG_STREAM_FORMAT_VALID; + uint32_t requested_subcmds = flags_host & valid_mask; + if (requested_subcmds & ~supported_mask) + return reply_not_supported(aecp, m, len); + } + + if (so->listener_observed) + return reply_status(aecp, AVB_AECP_AEM_STATUS_STREAM_IS_RUNNING, m, len); + + if (flags_host & AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID) { + uint32_t value = ntohl(cmd->msrp_accumulated_latency); + /* Section 5.4.2.9: range 0 .. 0x7FFFFFFF nanoseconds. */ + if (value > 0x7FFFFFFFu) + return reply_status(aecp, AVB_AECP_AEM_STATUS_BAD_ARGUMENTS, m, len); + if (so->presentation_time_offset_ns != value) { + so->presentation_time_offset_ns = value; + so->stream_info_dirty = true; + } + } + + /* Section 5.4.2.9: response echoes the command with the same flags. */ + return reply_success(aecp, m, len); +} + +void cmd_get_stream_info_emit_unsol_milan_v12(struct server *server, + uint16_t desc_type, uint16_t desc_index) +{ + uint8_t buf[AVB_AECP_GET_STREAM_INFO_RESPONSE_LEN]; + struct avb_ethernet_header *h; + struct avb_packet_aecp_aem *p; + struct avb_packet_aecp_aem_setget_stream_info *body; + struct descriptor *desc; + struct aecp_aem_base_info b_state = { 0 }; + struct aecp aecp_local = { .server = server }; + struct aecp *aecp = &aecp_local; + + desc = server_find_descriptor(aecp->server, desc_type, desc_index); + if (desc == NULL) + return; + if (desc_type != AVB_AEM_DESC_STREAM_INPUT && + desc_type != AVB_AEM_DESC_STREAM_OUTPUT) + return; + + memset(buf, 0, sizeof(buf)); + h = (struct avb_ethernet_header *)buf; + p = SPA_PTROFF(h, sizeof(*h), void); + body = (struct avb_packet_aecp_aem_setget_stream_info *)p->payload; + + AVB_PACKET_AECP_SET_MESSAGE_TYPE(&p->aecp, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + AVB_PACKET_AECP_SET_STATUS(&p->aecp, AVB_AECP_AEM_STATUS_SUCCESS); + AVB_PACKET_SET_LENGTH(&p->aecp.hdr, AVB_AECP_GET_STREAM_INFO_CDL); + p->cmd1 = 0; + p->cmd2 = AVB_AECP_AEM_CMD_GET_STREAM_INFO; + + body->descriptor_type = htons(desc_type); + body->descriptor_index = htons(desc_index); + + if (desc_type == AVB_AEM_DESC_STREAM_INPUT) + populate_input_response(aecp, + (struct aecp_aem_stream_input_state_milan_v12 *)desc->ptr, + body); + else + populate_output_response(aecp, desc_index, + (struct aecp_aem_stream_output_state *)desc->ptr, + body); + + (void)reply_unsolicited_notifications(aecp, &b_state, buf, + AVB_AECP_GET_STREAM_INFO_RESPONSE_LEN, true); +} diff --git a/src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.h b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.h new file mode 100644 index 000000000..467536340 --- /dev/null +++ b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-info.h @@ -0,0 +1,31 @@ +/* SPDX-FileCopyrightText: Copyright © 2025 Alexandre Malki */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __AVB_AECP_AEM_CMD_GET_SET_STREAM_INFO_H__ +#define __AVB_AECP_AEM_CMD_GET_SET_STREAM_INFO_H__ + +#include "../aecp-aem.h" + +int handle_cmd_set_stream_info_milan_v12(struct aecp *aecp, int64_t now, + const void *m, int len); + +int handle_cmd_get_stream_info_milan_v12(struct aecp *aecp, int64_t now, + const void *m, int len); + +/** + * \brief Emit an unsolicited GET_STREAM_INFO RESPONSE notification to all + * registered controllers for the given descriptor. Call after state + * transitions that change the GET_STREAM_INFO answer (BIND_RX, UNBIND_RX, + * probe complete, START/STOP_STREAMING) — controllers like Hive cache the + * last GET_STREAM_INFO response and don't auto-refetch on bind, so without + * this push their UI shows stale stream_id / dest_mac / vlan_id. + * + * Takes a server * (rather than aecp *) so callers in ACMP — which only + * hold the server — can invoke it without plumbing the AECP module ptr. + * + * \see Milan v1.2 Section 5.4.5 / IEEE 1722.1-2021 Section 7.5.2 + */ +void cmd_get_stream_info_emit_unsol_milan_v12(struct server *server, + uint16_t desc_type, uint16_t desc_index); + +#endif /* __AVB_AECP_AEM_CMD_GET_SET_STREAM_INFO_H__ */