Is there a way to stream audio from one ESPHome to another?

There is quite a lot of overhead in the underlying network stack (lwIP) in creating Berkeley sockets which is not a good idea to go through for every block of samples.

It would be better to create the socket once, use connect() to fix its remote endpoint (address, port tuple) and save the file descriptor (the return value from socket()) in a static. Subsequent data events can then just call send().

If the destination is always going to be a fixed address designated by its IP address, then you can use a local static as follows.

...
    on_data:
      - lambda: |-
          static int sock = -1;

          if (sock < 0) {
            sock = ::socket(AF_INET, SOCK_DGRAM, 0);
            if (sock >= 0) {
              static const struct sockaddr_in destination = {
                .sin_family = AF_INET,
                .sin_port = htons(12345),
                .sin_addr = { .s_addr = inet_addr("192.168.X.X") }
              };
              if (::connect(sock, reinterpret_cast<const struct sockaddr *>(&destination), sizeof(destination)) != 0) {
                (void) ::close(sock);
                sock = -1;
              }
            }
          }

          if (sock >= 0) {
            static std::vector<int16_t> audio_buffer;

            for (const auto sample : x) {
              audio_buffer.push_back(sample);
            }
            if (audio_buffer.size() >= 256) {
              (void) ::send(sock, audio_buffer.data(), audio_buffer.size() * sizeof(int16_t), 0);
              audio_buffer.clear();
            }
          }

If, on the other hand, the destination can change at runtime or is going to be given as a domain name and looked up using getaddrinfo() (getaddrinfo(3) - Linux manual page) then store the socket FD in a global: and have a lamdba script do the getaddrinfo(), socket(), connect() calls whenever the network state or the source domain name (e.g. from a template text: component) changes, remembering to close() any existing socket before creating a new one otherwise you’ll have a memory leak and run out of heap RAM eventually - causing a reboot. I’ll leave that as an exercise for the reader. I’ve added another post below with an example package.

Note that for UDP sockets, connect() doesn’t actually do any network transfers itself, it merely stores the default destination address in the socket data structure for send() to use later.

1 Like