SDL_net: Added an example VoIP group chat program.

From 3c57166387a516a2941947eb4b53a0f3ea9058ae Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Sun, 13 Aug 2023 19:45:53 -0400
Subject: [PATCH] Added an example VoIP group chat program.

Please don't use this for anything serious.  :)
---
 examples/voipchat.c | 375 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 375 insertions(+)
 create mode 100644 examples/voipchat.c

diff --git a/examples/voipchat.c b/examples/voipchat.c
new file mode 100644
index 0000000..d294ed8
--- /dev/null
+++ b/examples/voipchat.c
@@ -0,0 +1,375 @@
+#include <SDL3/SDL.h>
+#include "SDL_net.h"
+
+/*
+ * This is just for demonstration purposes! A real VoIP solution would
+ * definitely compress audio with a speech codec of some sort, it would
+ * deal with packet loss better, it would have encryption, NAT punching,
+ * and it wouldn't just allow _anyone_ to talk to you without some sort
+ * of authorization step.
+ *
+ * All this to say: don't use this for anything serious!
+ */
+
+typedef struct Voice
+{
+    SDL_AudioStream *stream;
+    SDLNet_Address *addr;
+    Uint16 port;
+    Uint64 idnum;
+    Uint64 last_seen;
+    Uint64 last_packetnum;
+    struct Voice *prev;
+    struct Voice *next;
+} Voice;
+
+static SDLNet_DatagramSocket *sock = NULL;  /* you talk over this, client or server. */
+static SDLNet_Address *server_addr = NULL;  /* address of the server you're talking to, NULL if you _are_ the server. */
+static Uint16 server_port = 3025;
+static int max_datagram = 0;
+static Voice *voices = NULL;
+static Uint64 next_idnum = 0;
+
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+static SDL_AudioDeviceID audio_device = 0;
+static SDL_AudioDeviceID capture_device = 0;
+static SDL_AudioStream *capture_stream = NULL;
+static const SDL_AudioSpec audio_spec = { SDL_AUDIO_S16LSB, 1, 8000 };
+static Uint8 scratch_area[4096];
+
+static Voice *FindVoiceByAddr(const SDLNet_Address *addr, const Uint16 port)
+{
+    Voice *i;
+    for (i = voices; i != NULL; i = i->next) {
+        if ((i->port == port) && (SDLNet_CompareAddresses(i->addr, addr) == 0)) {
+            return i;
+        }
+    }
+    return NULL;
+}
+
+static Voice *FindVoiceByIdNum(const Uint64 idnum)
+{
+    Voice *i;
+    for (i = voices; i != NULL; i = i->next) {
+        if (i->idnum == idnum) {
+            return i;
+        }
+    }
+    return NULL;
+}
+
+static void ClearOldVoices(const Uint64 now)
+{
+    Voice *i;
+    Voice *next;
+    for (i = voices; i != NULL; i = next) {
+        next = i->next;
+        if (!now || ((now - i->last_seen) > 60000)) {  /* nothing new in 60+ seconds? (or shutting down?) */
+            if (!i->stream || (SDL_GetAudioStreamAvailable(i->stream) == 0)) {  /* they'll get a reprieve if data is still playing out */
+                SDL_Log("Destroying voice #%llu", (unsigned long long) i->idnum);
+                SDL_DestroyAudioStream(i->stream);
+                SDLNet_UnrefAddress(i->addr);
+                if (i->prev) {
+                    i->prev->next = next;
+                } else {
+                    voices = next;
+                }
+                if (next) {
+                    next->prev = i->prev;
+                }
+                SDL_free(i);
+            }
+        }
+    }
+}
+
+static const size_t extra = (sizeof (Uint64) * 2);
+static void SendClientAudioToServer(void)
+{
+    const int br = SDL_GetAudioStreamData(capture_stream, scratch_area + extra, max_datagram - extra);
+    if (br > 0) {
+        ((Uint64 *) scratch_area)[0] = SDL_SwapLE64(0);  /* just being nice and leaving space in the buffer for the server to replace. */
+        ((Uint64 *) scratch_area)[1] = SDL_SwapLE64(++next_idnum);
+        SDL_Log("CLIENT: Sending %d new bytes to server at %s:%d...", (int) (br + extra), SDLNet_GetAddressString(server_addr), (int) server_port);
+        SDLNet_SendDatagram(sock, server_addr, server_port, scratch_area, br + extra);
+    }
+}
+
+static void mainloop(void)
+{
+    const SDL_bool is_client = (server_addr != NULL) ? SDL_TRUE : SDL_FALSE;
+    SDL_bool done = SDL_FALSE;
+    Uint64 last_send_ticks = 0;
+
+    if (is_client) {
+        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);  /* red by default (green when recording) */
+    }
+
+    while (!done) {
+        SDL_bool activity = SDL_FALSE;
+        const Uint64 now = SDL_GetTicks();
+        SDL_Event event;
+        SDLNet_Datagram *dgram = NULL;
+        int rc;
+
+        while (((rc = SDLNet_ReceiveDatagram(sock, &dgram)) == 0) && (dgram != NULL)) {
+            SDL_Log("%s: got %d-byte datagram from %s:%d", is_client ? "CLIENT" : "SERVER", (int) dgram->buflen, SDLNet_GetAddressString(dgram->addr), (int) dgram->port);
+            activity = SDL_TRUE;
+            if (!is_client) {  /* we're the server? */
+                Voice *voice = FindVoiceByAddr(dgram->addr, dgram->port);
+                Voice *i;
+
+                if (!voice) {
+                    SDL_Log("SERVER: Creating voice idnum=%llu from %s:%d", (unsigned long long) (next_idnum+1), SDLNet_GetAddressString(dgram->addr), (int) dgram->port);
+                    voice = (Voice *) SDL_calloc(1, sizeof (Voice));
+                    voice->addr = SDLNet_RefAddress(dgram->addr);
+                    voice->port = dgram->port;
+                    voice->idnum = ++next_idnum;
+                    if (voices) {
+                        voice->next = voices;
+                        voices->prev = voice;
+                    }
+                    voices = voice;
+                }
+
+                voice->last_seen = now;
+
+                /* send this new voice data to all recent speakers. */
+                if (dgram->buflen > extra) {  /* ignore it if too small, might just be a keepalive packet. */
+                    *((Uint64 *) dgram->buf) = SDL_SwapLE64(voice->idnum);  /* the client leaves space to fill this in for convenience. */
+                    for (i = voices; i != NULL; i = i->next) {
+                        if ((voice->port != i->port) || (SDLNet_CompareAddresses(voice->addr, i->addr) != 0)) {  /* don't send client's own voice back to them. */
+                            SDL_Log("SERVER: sending %d-byte datagram to %s:%d", (int) dgram->buflen, SDLNet_GetAddressString(i->addr), (int) i->port);
+                            SDLNet_SendDatagram(sock, i->addr, i->port, dgram->buf, dgram->buflen);
+                        }
+                    }
+                }
+            } else {  /* we're the client. */
+                if ((dgram->port != server_port) || (SDLNet_CompareAddresses(dgram->addr, server_addr) != 0)) {
+                    SDL_Log("CLIENT: Got packet from non-server address %s:%d. Ignoring.", SDLNet_GetAddressString(dgram->addr), (int) dgram->port);
+                } else if (dgram->buflen < (sizeof (Uint64) * 2)) {
+                    SDL_Log("CLIENT: Got bogus packet from the server. Ignoring.");
+                } else {
+                    const Uint64 idnum = SDL_SwapLE64(((const Uint64 *) dgram->buf)[0]);
+                    const Uint64 packetnum = SDL_SwapLE64(((const Uint64 *) dgram->buf)[1]);
+                    Voice *voice = FindVoiceByIdNum(idnum);
+                    if (!voice) {
+                        SDL_Log("CLIENT: Creating voice idnum=#%llu", (unsigned long long) idnum);
+                        voice = (Voice *) SDL_calloc(1, sizeof (Voice));
+                        if (audio_device) {
+                            voice->stream = SDL_CreateAndBindAudioStream(audio_device, &audio_spec);
+                        }
+                        voice->idnum = idnum;
+                        if (voices) {
+                            voice->next = voices;
+                            voices->prev = voice;
+                        }
+                        voices = voice;
+                    }
+
+                    voice->last_seen = now;
+
+                    if (packetnum > voice->last_packetnum) {   /* if packet arrived out of order, don't queue it for playing. */
+                        voice->last_packetnum = packetnum;
+                        SDL_PutAudioStreamData(voice->stream, dgram->buf + extra, dgram->buflen - extra);
+                        SDL_FlushAudioStream(voice->stream);  /* flush right away so we have all data if the stream dries up. We can still safely add more data later. */
+                    }
+                }
+            }
+
+            SDLNet_FreeDatagram(dgram);
+        }
+
+        while (SDL_PollEvent(&event)) {
+            activity = SDL_TRUE;
+            switch (event.type) {
+                case SDL_EVENT_QUIT:
+                    done = 1;
+                    break;
+
+                case SDL_EVENT_MOUSE_BUTTON_DOWN:
+                    if (is_client && capture_stream && (event.button.button == SDL_BUTTON_LEFT)) {
+                        if (SDL_BindAudioStream(capture_device, capture_stream) == 0) {
+                            SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);  /* green when recording */
+                        }
+                    }
+                    break;
+
+                case SDL_EVENT_MOUSE_BUTTON_UP:
+                    if (is_client && capture_stream && (event.button.button == SDL_BUTTON_LEFT)) {
+                        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);  /* red when not recording */
+                        SDL_UnbindAudioStream(capture_stream);
+                        SDL_FlushAudioStream(capture_stream);
+                        while (SDL_GetAudioStreamAvailable(capture_stream) > 0) {
+                            SendClientAudioToServer();
+                            last_send_ticks = now;
+                        }
+                    }
+                    break;
+            }
+        }
+
+        if (is_client) {
+            if (capture_stream) {
+                while (SDL_GetAudioStreamAvailable(capture_stream) > max_datagram) {
+                    SendClientAudioToServer();
+                    last_send_ticks = now;
+                    activity = SDL_TRUE;
+                }
+            }
+
+            if (!last_send_ticks || ((now - last_send_ticks) > 5000)) {  /* send a keepalive packet if we haven't transmitted for a bit. */
+                ((Uint64 *) scratch_area)[0] = SDL_SwapLE64(0);
+                ((Uint64 *) scratch_area)[1] = SDL_SwapLE64(++next_idnum);
+                SDL_Log("CLIENT: Sending %d keepalive bytes to server at %s:%d...", (int) extra, SDLNet_GetAddressString(server_addr), (int) server_port);
+                SDLNet_SendDatagram(sock, server_addr, server_port, scratch_area, extra);
+                last_send_ticks = now;
+            }
+        }
+
+        ClearOldVoices(now);
+
+        if (!activity) {
+            SDL_Delay(10);
+        }
+
+        SDL_RenderClear(renderer);
+        SDL_RenderPresent(renderer);
+    }
+}
+
+static void run_voipchat(int argc, char **argv)
+{
+    const char *hostname = NULL;
+    SDL_bool is_server = SDL_FALSE;
+    int simulate_failure = 0;
+    int i;
+
+    for (i = 1; i < argc; i++) {
+        const char *arg = argv[i];
+        if (SDL_strcmp(arg, "--server") == 0) {
+            is_server = SDL_TRUE;
+        } else if ((SDL_strcmp(arg, "--port") == 0) && (i < (argc-1))) {
+            server_port = (Uint16) SDL_atoi(argv[++i]);
+        } else if ((SDL_strcmp(arg, "--simulate-failure") == 0) && (i < (argc-1))) {
+            simulate_failure = (int) SDL_atoi(argv[++i]);
+        } else {
+            hostname = arg;
+        }
+    }
+
+    simulate_failure = SDL_min(SDL_max(simulate_failure, 0), 100);
+
+    if (simulate_failure) {
+        SDL_Log("Simulating failure at %d percent", simulate_failure);
+        SDLNet_SimulateAddressResolutionLoss(simulate_failure);
+    }
+
+    if (is_server && hostname) {
+        SDL_Log("WARNING: Specified --server and a hostname, ignoring the hostname");
+    } else if (!is_server && !hostname) {
+        SDL_Log("USAGE: %s <--server|hostname> [--port X] [--simulate-failure Y]", argv[0]);
+        return;
+    }
+
+    if (is_server) {
+        SDL_Log("SERVER: Listening on port %d", server_port);
+    } else {
+        SDL_Log("CLIENT: Resolving server hostname '%s' ...", hostname);
+        server_addr = SDLNet_ResolveHostname(hostname);
+        if (server_addr) {
+            if (SDLNet_WaitForResolution(server_addr) < 0) {
+                SDLNet_UnrefAddress(server_addr);
+                server_addr = NULL;
+            }
+        }
+
+        if (!server_addr) {
+            SDL_Log("CLIENT: Failed! %s", SDL_GetError());
+            SDL_Log("CLIENT: Giving up.");
+            return;
+        }
+
+        SDL_Log("CLIENT: Server is at %s:%d.", SDLNet_GetAddressString(server_addr), (int) server_port);
+
+        audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_OUTPUT, &audio_spec);
+        if (!audio_device) {
+            SDL_Log("CLIENT: Failed to open output audio device (%s), going on without sound playback!", SDL_GetError());
+        }
+
+        capture_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_CAPTURE, &audio_spec);
+        if (!capture_device) {
+            SDL_Log("CLIENT: Failed to open capture audio device (%s), going on without sound recording!", SDL_GetError());
+        } else {
+            capture_stream = SDL_CreateAudioStream(&audio_spec, &audio_spec);
+            if (!capture_stream) {
+                SDL_Log("CLIENT: Failed to create capture audio stream (%s), going on without sound recording!", SDL_GetError());
+                SDL_CloseAudioDevice(capture_device);
+                capture_device = 0;
+            }
+        }
+    }
+
+    /* server _must_ be on the requested port. Clients can take anything available, server will respond to where it sees it come from. */
+    sock = SDLNet_CreateDatagramSocket(NULL, is_server ? server_port : 0);
+    if (!sock) {
+        SDL_Log("Failed to create datagram socket: %s", SDL_GetError());
+    } else {
+        if (simulate_failure) {
+            SDLNet_SimulateDatagramPacketLoss(sock, simulate_failure);
+        }
+        mainloop();
+    }
+
+    SDL_Log("Shutting down...");
+
+    ClearOldVoices(0);
+
+    SDL_DestroyAudioStream(capture_stream);
+    SDL_CloseAudioDevice(audio_device);
+    SDL_CloseAudioDevice(capture_device);
+    audio_device = capture_device = 0;
+
+    SDLNet_UnrefAddress(server_addr);
+    server_addr = NULL;
+    SDLNet_DestroyDatagramSocket(sock);
+    sock = NULL;
+}
+
+
+int main(int argc, char **argv)
+{
+    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
+        SDL_Log("SDL_Init failed: %s\n", SDL_GetError());
+        return 1;
+    }
+
+    if (SDLNet_Init() < 0) {
+        SDL_Log("SDLNet_Init failed: %s\n", SDL_GetError());
+        SDL_Quit();
+        return 1;
+    }
+
+    window = SDL_CreateWindow("SDL_Net3 voipchat example", 640, 480, 0);
+    renderer = SDL_CreateRenderer(window, NULL, 0);
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+
+    max_datagram = 1500;
+    if (max_datagram > sizeof (scratch_area)) {
+        max_datagram = sizeof (scratch_area);
+    }
+
+    run_voipchat(argc, argv);
+
+    SDL_DestroyRenderer(renderer);
+    SDL_DestroyWindow(window);
+
+    SDLNet_Quit();
+    SDL_Quit();
+    return 0;
+}
+
+
+