Skip to content

VAFM Client Protocol

VAFM is Vexillum’s analog FM linking mode.

This page describes the current beta VAFM wire protocol and runtime behavior so client developers can build interoperable software clients, radio interface nodes, repeater adapters, gateways, and test tools.

Field Value
Protocol status Beta
Protocol version 0x01
Audio codec Opus
Audio format 48 kHz mono, 20 ms frames
Required transport profiles vafm+udp, vafm+tcp
Optional transport profiles vafm+tls, vafm+dtls, vafm+ws, vafm+wss

VAFM is under active development. Client implementations should be strict about parsing, tolerant of reconnects, and prepared for protocol changes while the beta design matures.

A VAFM client connects to a Vexillum VAFM instance using a supported transport profile, performs a simple session handshake, optionally authenticates with a shared passphrase, sends periodic keepalives, and transmits Opus audio frames when the user or attached radio is transmitting.

The current beta server behavior is a single shared room with one active talker at a time.

A client should handle:

  • Session setup with HELLO and WELCOME
  • Optional shared-passphrase authentication
  • A supported transport profile
  • Periodic keepalives
  • Opus voice packet transmission
  • Forwarded voice packet reception
  • Stream end detection
  • Explicit disconnects
  • Reconnects after transport failure

VAFM separates the packet protocol from the transport used to carry it.

A VAFM packet always uses the same header, packet type values, codec IDs, callsign encoding, stream behavior, authentication behavior, and payload rules regardless of transport. A transport profile only defines how complete VAFM packets are carried over the underlying network or local link.

This separation lets implementations support different deployment environments without creating a new VAFM dialect every time someone discovers another socket API and decides society needs to suffer.

Required transport profiles:

Profile Underlying transport Framing
vafm+udp UDP One complete VAFM packet per UDP datagram
vafm+tcp TCP Each VAFM packet is wrapped in a 2-byte big-endian length prefix

Optional transport profiles:

Profile Underlying transport Framing
vafm+tls TLS over TCP Same framing as vafm+tcp
vafm+dtls DTLS over UDP Same packet mapping as vafm+udp
vafm+ws WebSocket One complete VAFM packet per binary WebSocket message
vafm+wss WebSocket over TLS Same framing as vafm+ws

A VAFM server instance may listen on more than one transport profile. A client may use any profile supported by the server.

Implementations should treat the transport profile as a connection method only. Transport profiles must not change the VAFM packet format.

VAFM endpoints should be represented using URI-style profile names:

vafm+udp://reflector.example.net:43000
vafm+tcp://reflector.example.net:43000
vafm+tls://reflector.example.net:43443
vafm+dtls://reflector.example.net:43443
vafm+ws://reflector.example.net/vafm
vafm+wss://reflector.example.net/vafm

The URI scheme identifies the transport profile. The host, port, and path identify the server endpoint for that profile.

For profiles that do not use a path, clients should ignore an empty path or /.

For vafm+udp, each UDP datagram contains exactly one complete VAFM packet.

Clients must not split a VAFM packet across multiple UDP datagrams.

Clients should be prepared to:

  • Lose datagrams
  • Receive duplicate datagrams
  • Receive packets out of order
  • Re-send session setup or keepalive packets after network changes
  • Reconnect when no valid server traffic is received for the configured timeout interval

The vafm+udp profile is suitable for low-latency voice where occasional packet loss is preferable to delayed audio.

For vafm+tcp, each VAFM packet is length-prefixed:

uint16_be packet_length
byte[packet_length] vafm_packet

The length field is the size of the VAFM packet only. It does not include the two-byte TCP frame prefix.

TCP clients must handle:

  • Partial frames
  • Multiple frames in one read
  • Clean socket close
  • Unexpected socket close
  • Invalid frame lengths

A TCP stream parser should buffer incoming bytes and repeatedly extract complete frames.

The vafm+tcp profile is suitable for networks where UDP is blocked or unreliable, but clients should avoid allowing delayed voice frames to accumulate during network stalls.

The vafm+tls profile carries the vafm+tcp framing inside a TLS connection.

Packet framing is identical to vafm+tcp:

uint16_be packet_length
byte[packet_length] vafm_packet

The TLS layer provides transport encryption and server authentication when certificates are validated.

Clients using shared-passphrase authentication should prefer vafm+tls, vafm+dtls, vafm+wss, or another encrypted transport profile when connecting over untrusted networks.

A client must complete the TLS handshake before sending HELLO.

The vafm+dtls profile carries VAFM packets over DTLS.

Packet mapping is identical to vafm+udp:

one DTLS-protected datagram = one complete VAFM packet

Clients must not split a VAFM packet across multiple DTLS datagrams.

The vafm+dtls profile is suitable for encrypted low-latency voice where the implementation wants UDP-style packet behavior without exposing VAFM payloads directly on the network.

A client must complete the DTLS handshake before sending HELLO.

The vafm+ws profile carries VAFM packets over WebSocket.

Each binary WebSocket message contains exactly one complete VAFM packet.

Text WebSocket messages must not be used for VAFM packets.

VAFM packets must not be JSON-encoded, base64-encoded, or otherwise wrapped inside an application-level envelope. The WebSocket message payload is the VAFM packet.

The vafm+ws profile is useful for browser clients, web-based test tools, dashboards, and environments where WebSocket proxying is easier than raw TCP or UDP.

The vafm+wss profile is the secure WebSocket version of vafm+ws.

Packet mapping is identical to vafm+ws:

one binary WebSocket message = one complete VAFM packet

Clients using browser-based or webview-based implementations should prefer vafm+wss over vafm+ws when connecting over untrusted networks.

For vafm+ws and vafm+wss, each binary WebSocket message contains one complete VAFM packet.

A WebSocket client should:

  • Reject text messages
  • Reject empty binary messages
  • Validate each binary message as a complete VAFM packet
  • Drop malformed packets
  • Treat WebSocket close as a session disconnect
  • Reconnect using a new HELLO after transport failure

A WebSocket client must not assume that a WebSocket message contains JSON or any other wrapper. The binary message payload is the VAFM packet.

The current beta protocol does not negotiate transport profiles inside the VAFM packet stream.

A client selects a transport profile before sending HELLO, usually by configuration, URI scheme, service discovery, or user selection.

Servers should reject unsupported transport profiles at the transport layer rather than accepting a connection and later changing VAFM packet behavior.

The VAFM packet format does not provide encryption.

When shared-passphrase authentication is used, the AUTH payload contains the passphrase bytes. Implementations should avoid sending AUTH over unencrypted transport profiles on untrusted networks.

Secure deployments should prefer one of:

  • vafm+tls
  • vafm+dtls
  • vafm+wss

Plain vafm+udp, vafm+tcp, and vafm+ws may still be useful on trusted local networks, tunnels, VPNs, lab setups, and constrained embedded deployments.

All integer fields are big-endian.

Base header size: 32 bytes Maximum payload size: 4096 bytes

Offset Bytes Field Notes
0 4 Magic ASCII VAFM
4 1 Version Must be 0x01
5 1 Type Packet type enum
6 1 Flags Packet flags bitfield
7 1 Codec Codec enum
8 4 StreamID Unsigned 32-bit integer
12 4 Sequence Unsigned 32-bit integer
16 4 TimestampMS Unsigned 32-bit integer
20 10 Callsign Fixed-width bytes, space padded
30 2 PayloadLen Unsigned 16-bit integer
32 N Payload N = PayloadLen
  0                   1                   2                   3
b 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
B 0       1       2       3       4       5       6       7
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                           Magic VAFM                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |      Ver      |      Type     |     Flags     |     Codec     |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                            StreamID                           |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                            Sequence                           |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          TimestampMS                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                               |
 +                                                               +
 |                            Callsign                           |
 +                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                               |           PayloadLen          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                           Payload...                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

A client should validate every incoming packet before acting on it.

Required validation:

  • Magic must be VAFM.
  • Version must be 0x01.
  • Packet must be at least 32 bytes.
  • PayloadLen must match the actual payload size exactly.
  • PayloadLen must be less than or equal to 4096.
  • VOICE packets must use codec OPUS_48K_MONO_20MS.
  • For vafm+tcp and vafm+tls, the frame length must be greater than 0.
  • For vafm+tcp and vafm+tls, the frame length must be less than or equal to 32 + 4096.
  • For vafm+udp, vafm+dtls, vafm+ws, and vafm+wss, each datagram or binary message must contain exactly one complete VAFM packet.

Malformed packets should be dropped and logged as protocol errors.

Value Name Direction Purpose
0x01 HELLO Client to server Start or refresh a session
0x02 WELCOME Server to client Confirm session or authentication
0x03 AUTH Client to server Authenticate with shared passphrase
0x04 KEEPALIVE Both directions Keep the session alive
0x05 VOICE Both directions Carry Opus audio
0x06 END Both directions End the current transmission
0x07 ERROR Server to client Report protocol or authorization error
0x08 NAT_HINT Client to server Refresh liveness for NAT behavior
0x09 DISCONNECT Either direction Explicitly close the session
Value Name Purpose
0x01 EOT End of transmission
0x02 RELAY Reserved for future use

A stream ends when a client receives either:

  • A packet with type END
  • A VOICE packet with the EOT flag set
Value Name Purpose
0x00 NONE Control packets
0x01 OPUS_48K_MONO_20MS Voice packets

Expected usage:

Packet type Expected codec
HELLO NONE
WELCOME NONE
AUTH NONE
KEEPALIVE NONE
VOICE OPUS_48K_MONO_20MS
END NONE
ERROR NONE
NAT_HINT NONE
DISCONNECT NONE

The callsign field is exactly 10 bytes.

Encoding rules:

  • Use uppercase ASCII where possible.
  • Trim leading and trailing whitespace before encoding.
  • Right-pad with spaces, 0x20, when shorter than 10 bytes.
  • Truncate to 10 bytes when longer than 10 bytes.
  • Receivers should trim leading and trailing spaces when displaying.

Examples:

Callsign Encoded bytes
KC1AWV KC1AWV····
N0CALL N0CALL····
LONGCALL123 LONGCALL12

Spaces are shown as · for readability.

A client starts with HELLO.

Client                            Server
  |                                 |
  | HELLO                           |
  | callsign = client callsign      |
  |-------------------------------->|
  |                                 |
  | WELCOME                         |
  | callsign = server callsign      |
  |<--------------------------------|
  |                                 |

After WELCOME, the client may send keepalives and voice packets if authentication is not required.

A VAFM server instance may require a shared passphrase.

If authentication is configured, the client still sends HELLO first. After receiving WELCOME, the client sends AUTH with the raw passphrase bytes in the packet payload.

Because the current beta authentication payload contains the passphrase bytes, clients should prefer an encrypted transport profile such as vafm+tls, vafm+dtls, or vafm+wss when connecting over untrusted networks.

Client                       Server
  |                            |
  | HELLO                      |
  |--------------------------->|
  |                            |
  | WELCOME                    |
  |<---------------------------|
  |                            |
  | AUTH                       |
  | payload = passphrase bytes |
  |--------------------------->|
  |                            |
  | WELCOME                    |
  |<---------------------------|
  |                            |

On successful authentication, the server sends another WELCOME.

Clients must handle repeated WELCOME packets. A second WELCOME after AUTH is expected.

If authentication fails, the server sends:

type    = ERROR
payload = auth_failed

If a client sends VOICE or END before completing authentication on an authentication-required server, the server sends:

type    = ERROR
payload = auth_required

The unauthorized packet is not forwarded.

Clients should send periodic KEEPALIVE packets after a session is established.

Recommended interval:

20 to 30 seconds

Server behavior:

  • A client KEEPALIVE updates session liveness.
  • The server echoes KEEPALIVE back to the client.

Clients should treat a received KEEPALIVE as proof that the session is still alive.

NAT_HINT is accepted by the server and updates session liveness.

Current server behavior:

  • Accepts NAT_HINT
  • Updates liveness
  • Does not send a direct response

Clients may use NAT_HINT when they want to refresh NAT state without treating the packet as a full keepalive exchange.

Voice packets carry Opus audio.

Required voice format:

Field Value
Codec Opus
Sample rate 48 kHz
Channels Mono
Frame duration 20 ms
Packet codec ID 0x01

Each VOICE packet payload contains one encoded Opus frame.

A transmitting client should set:

Field Recommended behavior
Type VOICE
Codec OPUS_48K_MONO_20MS
StreamID Stable for one transmission
Sequence Increment for each voice frame
TimestampMS Monotonic packet timestamp in milliseconds
Callsign Client callsign
Payload Encoded Opus frame

The current beta server behaves as a single shared room with one active talker at a time.

Rules:

  • The first accepted VOICE packet opens the active stream.
  • While a stream is active, only packets from that same sender are accepted.
  • VOICE and END packets from other senders are ignored while another sender is active.
  • Accepted voice packets are forwarded to all other authenticated sessions.
  • The sender does not receive its own forwarded voice packets.
  • A stream ends on END or EOT.
  • The server also closes an idle stream if no frame is seen for about 3 seconds.

A client can end a transmission in either of two ways:

  1. Send an END packet.
  2. Set the EOT flag on the final VOICE packet.

Recommended behavior:

  • Use a stable StreamID for all packets in a transmission.
  • Increment Sequence for each voice frame.
  • Send END when PTT is released.
  • Also support receiving either END or EOT from other clients.

Example end packet:

Type      = END
Flags     = EOT optional
Codec     = NONE
StreamID  = current stream ID
Sequence  = final sequence value
Callsign  = client callsign
Payload   = empty

Server session behavior:

Condition Behavior
No activity for about 120 seconds Session times out
Stream-oriented transport disconnects Session ends immediately
Datagram-oriented transport stops receiving valid traffic Session times out after the normal inactivity interval
Inbound DISCONNECT Session is removed immediately
Server shutdown Server may send DISCONNECT

Activity includes:

  • HELLO
  • AUTH
  • KEEPALIVE
  • VOICE
  • END
  • NAT_HINT
  • DISCONNECT

DISCONNECT explicitly closes a VAFM session.

Either side may send it.

After receiving DISCONNECT, the peer should stop using the session immediately.

For datagram-oriented profiles such as vafm+udp, vafm+dtls, and other datagram voice delivery, DISCONNECT is the explicit teardown signal because there may be no socket close event visible to the VAFM packet layer.

The payload is optional and may contain an ASCII reason.

Common reason examples:

client_shutdown
server_shutdown

Current server behavior:

  • Removes the session immediately on inbound DISCONNECT
  • Closes the underlying stream connection for stream-oriented sessions

ERROR packets contain an ASCII error reason in the payload.

Current known error payloads:

Payload Meaning
auth_failed The provided shared passphrase was incorrect
auth_required The client attempted voice or end behavior before authentication

Clients should surface errors to logs or UI and decide whether to retry, prompt the user, or disconnect.

A VAFM client can be modeled with these states:

DISCONNECTED
  |
  v
CONNECTING
  |
  v
WAIT_WELCOME
  |
  +-- no auth required --> READY
  |
  +-- auth required --> AUTHENTICATING
                         |
                         v
                       READY
                         |
                         +-- PTT active --> TRANSMITTING
                         |
                         +-- incoming voice --> RECEIVING

Terminal conditions:

  • Transport failure
  • Session timeout
  • Inbound DISCONNECT
  • Authentication failure
  • User shutdown

A robust client should:

  • Strictly validate packet magic, version, lengths, and codec.
  • Encode callsigns as fixed-width 10-byte fields.
  • Send HELLO before transmitting.
  • Handle repeated WELCOME packets.
  • Send AUTH when required.
  • Send KEEPALIVE every 20 to 30 seconds.
  • Use Opus 48 kHz mono 20 ms frames for voice.
  • Use one StreamID per transmission.
  • Increment Sequence for each voice packet.
  • Send END or EOT when PTT ends.
  • Treat DISCONNECT as terminal for the current session.
  • Reconnect with backoff after transport failure.
  • For stream-oriented profiles such as vafm+tcp and vafm+tls, implement buffered frame parsing.
  • For datagram-oriented profiles such as vafm+udp and vafm+dtls, tolerate packet loss and reordering.
  • For message-oriented profiles such as vafm+ws and vafm+wss, treat each binary message as one complete VAFM packet.
  • Drop malformed packets instead of trying to repair them.
function build_packet(type, flags, codec, stream_id, sequence, timestamp_ms, callsign, payload):
    if payload.length > 4096:
        error "payload too large"

    if type == VOICE and codec != OPUS_48K_MONO_20MS:
        error "voice packet must use opus codec"

    encoded_callsign = uppercase(trim(callsign))
    encoded_callsign = first 10 bytes of encoded_callsign
    encoded_callsign = right_pad_with_spaces(encoded_callsign, 10)

    header = byte[32]
    header[0..3]   = "VAFM"
    header[4]      = 0x01
    header[5]      = type
    header[6]      = flags
    header[7]      = codec
    header[8..11]  = uint32_be(stream_id)
    header[12..15] = uint32_be(sequence)
    header[16..19] = uint32_be(timestamp_ms)
    header[20..29] = encoded_callsign
    header[30..31] = uint16_be(payload.length)

    return header + payload
connect transport
send HELLO(callsign)

wait for WELCOME

if server requires authentication:
    send AUTH(passphrase bytes)
    wait for WELCOME or ERROR

start keepalive timer

while connected:
    if keepalive timer fires:
        send KEEPALIVE

    if ptt starts:
        stream_id = random uint32
        sequence = 0

        while ptt is active:
            opus_frame = read_encoded_opus_frame()
            send VOICE(
                codec = OPUS_48K_MONO_20MS,
                stream_id = stream_id,
                sequence = sequence,
                timestamp_ms = monotonic_ms(),
                payload = opus_frame
            )
            sequence += 1

        send END(stream_id = stream_id, sequence = sequence)

    if packet received:
        validate packet

        if packet.type == VOICE:
            play_or_route_opus_payload(packet.payload)

        if packet.type == END or packet.flags has EOT:
            close_receive_stream(packet.stream_id)

        if packet.type == KEEPALIVE:
            update_liveness()

        if packet.type == ERROR:
            log_or_display_error(packet.payload)

        if packet.type == DISCONNECT:
            close transport
            enter DISCONNECTED
buffer = empty bytes

while socket is open:
    bytes = read_from_socket()
    if bytes is empty:
        disconnect

    append bytes to buffer

    while buffer has at least 2 bytes:
        frame_len = uint16_be(buffer[0..1])

        if frame_len == 0:
            protocol error

        if frame_len > 32 + 4096:
            protocol error

        if buffer length < 2 + frame_len:
            break

        frame = buffer[2 .. 2 + frame_len]
        buffer = buffer[2 + frame_len .. end]

        packet = parse_vafm_packet(frame)
        handle_packet(packet)

Before testing a client against Vexillum, confirm:

  • Supports at least one required transport profile: vafm+udp or vafm+tcp.
  • Does not change VAFM packet format based on transport profile.
  • HELLO / WELCOME works over vafm+udp.
  • HELLO / WELCOME works over vafm+tcp.
  • Uses 2-byte big-endian length framing only for stream-oriented profiles that require it.
  • Sends exactly one complete VAFM packet per datagram or binary message on datagram/message-oriented profiles.
  • Stream reader handles partial frames.
  • Stream reader handles multiple frames in one read.
  • Datagram/message-oriented reader rejects empty packets or malformed packet boundaries.
  • Callsign encoding uses 10-byte padded fields.
  • AUTH success path works when authentication is required.
  • AUTH failure path handles ERROR auth_failed.
  • Unauthorized voice handles ERROR auth_required.
  • Prefers encrypted profiles when shared-passphrase authentication is used over untrusted networks.
  • KEEPALIVE is sent periodically.
  • KEEPALIVE echo is handled.
  • DISCONNECT tears down state cleanly.
  • Voice uses codec 0x01.
  • Voice payloads are less than or equal to 4096 bytes.
  • One transmission uses a stable StreamID.
  • Sequence increments during transmission.
  • END terminates local transmit state.
  • EOT terminates local transmit state.
  • Incoming VOICE packets are decoded as Opus 48 kHz mono 20 ms frames.
  • Incoming END or EOT closes receive stream state.
  • Reconnect uses backoff after transport failure.

VAFM voice payloads are Opus frames, not raw PCM.

Input audio may be PCM inside the client, but the packet payload must contain encoded Opus audio.

VAFM expects 48 kHz mono 20 ms Opus frames.

Do not send 8 kHz, stereo, variable-length capture chunks, or arbitrary audio buffers as voice payloads.

For vafm+tcp and vafm+tls, every VAFM packet is wrapped in a 2-byte big-endian length prefix.

Do not send raw VAFM packets directly over these stream-oriented profiles.

Correct stream frame layout:

uint16_be packet_length
byte[packet_length] vafm_packet

For datagram-oriented profiles such as vafm+udp and vafm+dtls, do not add the 2-byte stream prefix.

Assuming one stream read equals one packet

Section titled “Assuming one stream read equals one packet”

Stream-oriented transports do not preserve application message boundaries.

A single read may contain:

  • Part of one frame
  • One complete frame
  • Multiple complete frames
  • The end of one frame and the beginning of another

Clients using vafm+tcp or vafm+tls must maintain a receive buffer and extract complete length-prefixed frames from that buffer.

When authentication is required, the server sends WELCOME after HELLO and again after successful AUTH.

Clients should treat this as normal.

When PTT ends, send END or set the EOT flag on the final VOICE packet.

Do not rely on idle timeout as the normal end-of-transmission signal.

If using authentication, treat the shared passphrase as a secret. Do not log it, display it in crash output, or commit it into example configurations.

When shared-passphrase authentication is enabled, clients should use an encrypted transport profile unless the connection is already protected by a trusted tunnel, VPN, or private local network. Plain vafm+udp, vafm+tcp, and vafm+ws do not encrypt the AUTH payload.

A basic HELLO packet for callsign KC1AWV with no payload:

Field Value
Magic VAFM
Version 0x01
Type 0x01
Flags 0x00
Codec 0x00
StreamID 0
Sequence 0
TimestampMS 0
Callsign KC1AWV····
PayloadLen 0
Payload empty

A VOICE packet:

Field Value
Magic VAFM
Version 0x01
Type 0x05
Flags 0x00, or 0x01 for final frame
Codec 0x01
StreamID Stable ID for this transmission
Sequence Incrementing frame number
TimestampMS Monotonic timestamp
Callsign Client callsign
PayloadLen Opus frame length
Payload Encoded Opus frame

The current protocol version is:

0x01

Clients must reject unsupported versions.

Future protocol versions may add fields, packet types, flags, authentication modes, room behavior, or metadata. Clients should keep the parser strict for the current version and make version handling explicit.

VAFM is implemented as a Vexillum mode runtime. Vexillum provides the server-side implementation of the VAFM protocol, and client developers can build interoperable software that connects to it.

Client developers do not need to use Go or link against Vexillum. The protocol is intentionally simple enough to implement in any language with UDP or TCP networking and Opus support.

The current beta behavior has several intentional limitations:

  • One shared room
  • One active talker at a time
  • Shared-passphrase authentication only
  • No negotiated codec options
  • No room or talkgroup selection in the packet format
  • No end-to-end encryption
  • No retransmission or jitter buffer behavior specified by the protocol

Client implementations may add their own local audio buffering, device management, reconnect logic, logging, configuration, and user interface behavior.

When building a VAFM client, start small:

  1. Implement packet encode/decode.
  2. Add UDP HELLO / WELCOME.
  3. Add TCP framing.
  4. Add keepalive.
  5. Add authentication.
  6. Add DISCONNECT.
  7. Add Opus encode/decode.
  8. Add receive audio playback.
  9. Add transmit audio capture.
  10. Add PTT and stream end behavior.
  11. Add reconnect and error handling.
  12. Test against both required transport profiles: vafm+udp and vafm+tcp.
  13. If implemented, test encrypted and message-oriented profiles such as vafm+tls, vafm+dtls, vafm+ws, and vafm+wss.

Keep packet handling separate from audio device handling. It makes testing easier and keeps your protocol implementation from becoming tangled with whatever audio stack your operating system decided to invent this decade.

This document reflects current VAFM beta behavior.

When server behavior changes, update this document and the associated protocol tests together. Client developers should track protocol version changes and test against the Vexillum server implementation they intend to support.