diff --git a/Makefile b/Makefile index 2aadb93..88dc5c1 100644 --- a/Makefile +++ b/Makefile @@ -148,16 +148,24 @@ SRCS := src/main.c \ src/vaapi_encoder.c \ src/vaapi_decoder.c \ src/nvenc_encoder.c \ + src/ffmpeg_encoder.c \ + src/raw_encoder.c \ src/display_sdl2.c \ src/opus_codec.c \ src/audio_capture.c \ + src/audio_capture_pulse.c \ + src/audio_capture_dummy.c \ src/audio_playback.c \ + src/audio_playback_pulse.c \ + src/audio_playback_dummy.c \ src/network.c \ src/network_tcp.c \ src/network_reconnect.c \ src/input.c \ src/crypto.c \ src/discovery.c \ + src/discovery_broadcast.c \ + src/discovery_manual.c \ src/tray.c \ src/service.c \ src/qrcode.c \ diff --git a/include/rootstream.h b/include/rootstream.h index 3fb3725..83059c5 100644 --- a/include/rootstream.h +++ b/include/rootstream.h @@ -348,6 +348,14 @@ typedef struct { * DISCOVERY - mDNS/Avahi service discovery * ============================================================================ */ +/* Peer history entry for quick reconnection (PHASE 5) */ +typedef struct { + char hostname[256]; + char address[256]; /* IP:port format */ + uint16_t port; + char rootstream_code[ROOTSTREAM_CODE_MAX_LEN]; +} peer_history_entry_t; + typedef struct { void *avahi_client; /* Avahi client (opaque) */ void *avahi_group; /* Avahi entry group (opaque) */ @@ -476,6 +484,10 @@ typedef struct rootstream_ctx { /* Discovery */ discovery_ctx_t discovery; + + /* Peer history (PHASE 5) */ + peer_history_entry_t peer_history_entries[MAX_PEER_HISTORY]; + int num_peer_history; /* Input */ int uinput_kbd_fd; /* Virtual keyboard */ @@ -739,6 +751,20 @@ int discovery_announce(rootstream_ctx_t *ctx); int discovery_browse(rootstream_ctx_t *ctx); void discovery_cleanup(rootstream_ctx_t *ctx); +/* Discovery - Broadcast (PHASE 5) */ +int discovery_broadcast_announce(rootstream_ctx_t *ctx); +int discovery_broadcast_listen(rootstream_ctx_t *ctx, int timeout_ms); + +/* Discovery - Manual (PHASE 5) */ +int discovery_manual_add_peer(rootstream_ctx_t *ctx, const char *address_or_code); +int discovery_save_peer_to_history(rootstream_ctx_t *ctx, const char *hostname, + uint16_t port, const char *rootstream_code); +void discovery_list_peer_history(rootstream_ctx_t *ctx); +int discovery_parse_address(const char *address, char *hostname, uint16_t *port); +int discovery_parse_rootstream_code(rootstream_ctx_t *ctx, const char *code, + char *hostname, uint16_t *port); + + /* --- Input (existing, polished) --- */ int rootstream_input_init(rootstream_ctx_t *ctx); int rootstream_input_process(rootstream_ctx_t *ctx, input_event_pkt_t *event); diff --git a/src/audio_capture_pulse.c b/src/audio_capture_pulse.c index fd2c034..e093afb 100644 --- a/src/audio_capture_pulse.c +++ b/src/audio_capture_pulse.c @@ -132,6 +132,7 @@ int audio_capture_init_pulse(rootstream_ctx_t *ctx) { return 0; #else + (void)ctx; fprintf(stderr, "ERROR: PulseAudio support not compiled\n"); return -1; #endif diff --git a/src/audio_playback_pulse.c b/src/audio_playback_pulse.c index 5151c78..bde82d2 100644 --- a/src/audio_playback_pulse.c +++ b/src/audio_playback_pulse.c @@ -128,6 +128,7 @@ int audio_playback_init_pulse(rootstream_ctx_t *ctx) { return 0; #else + (void)ctx; fprintf(stderr, "ERROR: PulseAudio support not compiled\n"); return -1; #endif diff --git a/src/discovery.c b/src/discovery.c index ce25d72..503d38f 100644 --- a/src/discovery.c +++ b/src/discovery.c @@ -25,6 +25,9 @@ #include #include +/* Discovery timeout for UDP broadcast (milliseconds) */ +#define BROADCAST_DISCOVERY_TIMEOUT_MS 1000 + #ifdef HAVE_AVAHI #include #include @@ -261,7 +264,7 @@ static void client_callback(AvahiClient *c, AvahiClientState state, #endif /* HAVE_AVAHI */ /* - * Initialize discovery system + * Initialize discovery system with fallback support (PHASE 5) */ int discovery_init(rootstream_ctx_t *ctx) { if (!ctx) { @@ -269,11 +272,16 @@ int discovery_init(rootstream_ctx_t *ctx) { return -1; } + printf("INFO: Initializing peer discovery...\n"); + #ifdef HAVE_AVAHI + /* Try mDNS/Avahi first (Tier 1) */ + printf("INFO: Attempting discovery backend: mDNS/Avahi\n"); + avahi_ctx_t *avahi = calloc(1, sizeof(avahi_ctx_t)); if (!avahi) { - fprintf(stderr, "ERROR: Cannot allocate Avahi context\n"); - return -1; + fprintf(stderr, "WARNING: Cannot allocate Avahi context\n"); + goto try_broadcast; } avahi->ctx = ctx; @@ -282,8 +290,8 @@ int discovery_init(rootstream_ctx_t *ctx) { avahi->simple_poll = avahi_simple_poll_new(); if (!avahi->simple_poll) { free(avahi); - fprintf(stderr, "ERROR: Cannot create Avahi poll object\n"); - return -1; + fprintf(stderr, "WARNING: Cannot create Avahi poll object\n"); + goto try_broadcast; } /* Create client */ @@ -296,135 +304,158 @@ int discovery_init(rootstream_ctx_t *ctx) { &error); if (!avahi->client) { - fprintf(stderr, "ERROR: Cannot create Avahi client: %s\n", + fprintf(stderr, "WARNING: Cannot create Avahi client: %s\n", avahi_strerror(error)); avahi_simple_poll_free(avahi->simple_poll); free(avahi); - return -1; + goto try_broadcast; } ctx->discovery.avahi_client = avahi; ctx->discovery.running = true; - printf("✓ Discovery initialized (Avahi)\n"); + printf("✓ Discovery backend 'mDNS/Avahi' initialized\n"); return 0; -#else - fprintf(stderr, "WARNING: Built without Avahi support\n"); - fprintf(stderr, "INFO: Auto-discovery disabled\n"); - return 0; +try_broadcast: + fprintf(stderr, "WARNING: mDNS/Avahi failed, trying next...\n"); #endif + + /* Try UDP Broadcast (Tier 2) */ + printf("INFO: Attempting discovery backend: UDP Broadcast\n"); + + /* UDP broadcast doesn't need initialization, just mark discovery as available */ + ctx->discovery.running = true; + + printf("✓ Discovery backend 'UDP Broadcast' initialized\n"); + printf("INFO: Manual peer entry also available (--peer-add)\n"); + + return 0; } /* - * Announce service on network + * Announce service on network (with fallback support - PHASE 5) */ int discovery_announce(rootstream_ctx_t *ctx) { if (!ctx) return -1; #ifdef HAVE_AVAHI avahi_ctx_t *avahi = (avahi_ctx_t*)ctx->discovery.avahi_client; - if (!avahi || !avahi->client) { - fprintf(stderr, "ERROR: Discovery not initialized\n"); - return -1; - } - - /* Create entry group if needed */ - if (!avahi->group) { - avahi->group = avahi_entry_group_new(avahi->client, - entry_group_callback, avahi); + if (avahi && avahi->client) { + /* mDNS/Avahi is available, use it */ + + /* Create entry group if needed */ if (!avahi->group) { - fprintf(stderr, "ERROR: Cannot create Avahi entry group\n"); - return -1; + avahi->group = avahi_entry_group_new(avahi->client, + entry_group_callback, avahi); + if (!avahi->group) { + fprintf(stderr, "WARNING: Cannot create Avahi entry group\n"); + goto try_broadcast; + } } - } - /* Prepare TXT records */ - AvahiStringList *txt = NULL; - char version_txt[64], pubkey_txt[256]; - - snprintf(version_txt, sizeof(version_txt), "version=%s", ROOTSTREAM_VERSION); - txt = avahi_string_list_add(txt, version_txt); - - snprintf(pubkey_txt, sizeof(pubkey_txt), "code=%s", - ctx->keypair.rootstream_code); - txt = avahi_string_list_add(txt, pubkey_txt); - - /* Add service */ - int ret = avahi_entry_group_add_service_strlst( - avahi->group, - AVAHI_IF_UNSPEC, /* All interfaces */ - AVAHI_PROTO_UNSPEC, /* IPv4 and IPv6 */ - 0, /* flags */ - ctx->keypair.identity, /* Service name */ - "_rootstream._udp", /* Service type */ - NULL, /* Domain (use default) */ - NULL, /* Host (use default) */ - ctx->port, /* Port */ - txt); /* TXT records */ - - avahi_string_list_free(txt); - - if (ret < 0) { - fprintf(stderr, "ERROR: Cannot add service: %s\n", - avahi_strerror(ret)); - return -1; - } + /* Prepare TXT records */ + AvahiStringList *txt = NULL; + char version_txt[64], pubkey_txt[256]; + + snprintf(version_txt, sizeof(version_txt), "version=%s", ROOTSTREAM_VERSION); + txt = avahi_string_list_add(txt, version_txt); + + snprintf(pubkey_txt, sizeof(pubkey_txt), "code=%s", + ctx->keypair.rootstream_code); + txt = avahi_string_list_add(txt, pubkey_txt); + + /* Add service */ + int ret = avahi_entry_group_add_service_strlst( + avahi->group, + AVAHI_IF_UNSPEC, /* All interfaces */ + AVAHI_PROTO_UNSPEC, /* IPv4 and IPv6 */ + 0, /* flags */ + ctx->keypair.identity, /* Service name */ + "_rootstream._udp", /* Service type */ + NULL, /* Domain (use default) */ + NULL, /* Host (use default) */ + ctx->port, /* Port */ + txt); /* TXT records */ + + avahi_string_list_free(txt); + + if (ret < 0) { + fprintf(stderr, "WARNING: Cannot add service: %s\n", + avahi_strerror(ret)); + goto try_broadcast; + } - /* Commit changes */ - ret = avahi_entry_group_commit(avahi->group); - if (ret < 0) { - fprintf(stderr, "ERROR: Cannot commit entry group: %s\n", - avahi_strerror(ret)); - return -1; - } + /* Commit changes */ + ret = avahi_entry_group_commit(avahi->group); + if (ret < 0) { + fprintf(stderr, "WARNING: Cannot commit entry group: %s\n", + avahi_strerror(ret)); + goto try_broadcast; + } - printf("→ Announcing service on network\n"); - return 0; + printf("→ Announcing service on network (mDNS)\n"); + return 0; + } -#else - (void)ctx; - return 0; +try_broadcast: #endif + + /* Try UDP broadcast as fallback */ + if (discovery_broadcast_announce(ctx) == 0) { + printf("→ Announcing service on network (UDP broadcast)\n"); + return 0; + } + + fprintf(stderr, "WARNING: All discovery announce methods failed\n"); + fprintf(stderr, "INFO: Peers can still connect manually (--peer-add)\n"); + return 0; /* Not fatal - manual entry always works */ } /* - * Browse for services on network + * Browse for services on network (with fallback support - PHASE 5) */ int discovery_browse(rootstream_ctx_t *ctx) { if (!ctx) return -1; #ifdef HAVE_AVAHI avahi_ctx_t *avahi = (avahi_ctx_t*)ctx->discovery.avahi_client; - if (!avahi || !avahi->client) { - fprintf(stderr, "ERROR: Discovery not initialized\n"); - return -1; - } + if (avahi && avahi->client) { + /* mDNS/Avahi is available, use it */ + + /* Create browser */ + avahi->browser = avahi_service_browser_new( + avahi->client, + AVAHI_IF_UNSPEC, /* All interfaces */ + AVAHI_PROTO_UNSPEC, /* IPv4 and IPv6 */ + "_rootstream._udp", /* Service type */ + NULL, /* Domain (use default) */ + 0, /* flags */ + browse_callback, + avahi); + + if (!avahi->browser) { + fprintf(stderr, "WARNING: Cannot create service browser: %s\n", + avahi_strerror(avahi_client_errno(avahi->client))); + goto try_broadcast; + } - /* Create browser */ - avahi->browser = avahi_service_browser_new( - avahi->client, - AVAHI_IF_UNSPEC, /* All interfaces */ - AVAHI_PROTO_UNSPEC, /* IPv4 and IPv6 */ - "_rootstream._udp", /* Service type */ - NULL, /* Domain (use default) */ - 0, /* flags */ - browse_callback, - avahi); - - if (!avahi->browser) { - fprintf(stderr, "ERROR: Cannot create service browser: %s\n", - avahi_strerror(avahi_client_errno(avahi->client))); - return -1; + printf("→ Browsing for RootStream peers (mDNS)...\n"); + return 0; } - printf("→ Browsing for RootStream peers...\n"); - return 0; +try_broadcast: +#endif -#else - (void)ctx; + /* Try UDP broadcast as fallback */ + printf("→ Browsing for RootStream peers (UDP broadcast)...\n"); + + /* Listen for broadcast announcements with a short timeout */ + if (discovery_broadcast_listen(ctx, BROADCAST_DISCOVERY_TIMEOUT_MS) > 0) { + printf(" Found peer via broadcast\n"); + } + return 0; -#endif } /* diff --git a/src/discovery_broadcast.c b/src/discovery_broadcast.c new file mode 100644 index 0000000..4fd72cd --- /dev/null +++ b/src/discovery_broadcast.c @@ -0,0 +1,238 @@ +/* + * discovery_broadcast.c - UDP broadcast peer discovery + * + * Falls back to broadcast when mDNS unavailable. + * Broadcasts a discovery packet on local subnet and waits for responses. + * Works on any LAN without requiring Avahi. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DISCOVERY_BROADCAST_PORT 5555 +#define DISCOVERY_MAGIC "ROOTSTREAM_DISCOVER" + +typedef struct { + uint8_t magic[20]; /* "ROOTSTREAM_DISCOVER" */ + uint32_t version; /* Protocol version */ + char hostname[256]; /* Sender hostname */ + uint16_t listen_port; /* Port peer is listening on */ + char rootstream_code[ROOTSTREAM_CODE_MAX_LEN]; /* User's RootStream code */ +} discovery_broadcast_packet_t; + +/* + * Get local IP address for broadcast + */ +static int get_local_ip(char *ip_buf, size_t ip_len, char *bcast_buf, size_t bcast_len) { + struct ifaddrs *ifaddr, *ifa; + int ret = -1; + + if (getifaddrs(&ifaddr) == -1) { + perror("getifaddrs"); + return -1; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) continue; + + /* Skip loopback and non-IPv4 */ + if (ifa->ifa_addr->sa_family != AF_INET) continue; + if (strcmp(ifa->ifa_name, "lo") == 0) continue; + + struct sockaddr_in *sin = (struct sockaddr_in *)ifa->ifa_addr; + struct sockaddr_in *broadcast = (struct sockaddr_in *)ifa->ifa_broadaddr; + + inet_ntop(AF_INET, &sin->sin_addr, ip_buf, ip_len); + if (broadcast) { + inet_ntop(AF_INET, &broadcast->sin_addr, bcast_buf, bcast_len); + } else { + snprintf(bcast_buf, bcast_len, "255.255.255.255"); + } + + printf("✓ Using interface %s (%s, broadcast %s)\n", + ifa->ifa_name, ip_buf, bcast_buf); + ret = 0; + break; + } + + freeifaddrs(ifaddr); + return ret; +} + +/* + * Broadcast discovery query + */ +int discovery_broadcast_announce(rootstream_ctx_t *ctx) { + if (!ctx) return -1; + + char local_ip[INET_ADDRSTRLEN]; + char broadcast_ip[INET_ADDRSTRLEN]; + + if (get_local_ip(local_ip, sizeof(local_ip), + broadcast_ip, sizeof(broadcast_ip)) < 0) { + fprintf(stderr, "ERROR: Cannot determine local IP\n"); + return -1; + } + + int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + perror("socket"); + return -1; + } + + /* Enable broadcast */ + int broadcast_flag = 1; + if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast_flag, + sizeof(broadcast_flag)) < 0) { + perror("setsockopt SO_BROADCAST"); + close(sock); + return -1; + } + + /* Create announcement packet */ + discovery_broadcast_packet_t pkt; + memset(&pkt, 0, sizeof(pkt)); + memcpy(pkt.magic, DISCOVERY_MAGIC, strlen(DISCOVERY_MAGIC)); + pkt.version = PROTOCOL_VERSION; + gethostname(pkt.hostname, sizeof(pkt.hostname)); + pkt.listen_port = ctx->port; + strncpy(pkt.rootstream_code, ctx->keypair.rootstream_code, + sizeof(pkt.rootstream_code) - 1); + + struct sockaddr_in bcast_addr = { + .sin_family = AF_INET, + .sin_port = htons(DISCOVERY_BROADCAST_PORT), + }; + inet_aton(broadcast_ip, &bcast_addr.sin_addr); + + /* Send broadcast */ + ssize_t ret = sendto(sock, &pkt, sizeof(pkt), 0, + (struct sockaddr *)&bcast_addr, sizeof(bcast_addr)); + close(sock); + + if (ret < 0) { + perror("sendto"); + return -1; + } + + printf("✓ Broadcast discovery announced (%s:%u)\n", local_ip, ctx->port); + return 0; +} + +/* + * Listen for broadcast discovery queries and respond + */ +int discovery_broadcast_listen(rootstream_ctx_t *ctx, int timeout_ms) { + if (!ctx) return -1; + + int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + perror("socket"); + return -1; + } + + /* Bind to broadcast port */ + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_ANY), + .sin_port = htons(DISCOVERY_BROADCAST_PORT), + }; + + if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(sock); + return -1; + } + + /* Poll with timeout */ + struct pollfd pfd = { .fd = sock, .events = POLLIN }; + int poll_ret = poll(&pfd, 1, timeout_ms); + + if (poll_ret <= 0) { + close(sock); + return poll_ret; /* No data or timeout */ + } + + /* Receive broadcast packet */ + discovery_broadcast_packet_t pkt; + struct sockaddr_in from_addr; + socklen_t from_len = sizeof(from_addr); + + ssize_t recv_len = recvfrom(sock, &pkt, sizeof(pkt), 0, + (struct sockaddr *)&from_addr, &from_len); + close(sock); + + if (recv_len < 0) { + perror("recvfrom"); + return -1; + } + + if (recv_len != sizeof(pkt)) { + fprintf(stderr, "WARNING: Invalid discovery packet size\n"); + return 0; + } + + /* Validate magic */ + if (memcmp(pkt.magic, DISCOVERY_MAGIC, strlen(DISCOVERY_MAGIC)) != 0) { + return 0; /* Not a RootStream packet */ + } + + /* Found a peer! */ + char peer_ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &from_addr.sin_addr, peer_ip, sizeof(peer_ip)); + + printf("✓ Discovered peer: %s (%s:%u, code: %.16s...)\n", + pkt.hostname, peer_ip, ntohs(pkt.listen_port), pkt.rootstream_code); + + /* Add to peer list if not already present */ + bool already_exists = false; + for (int i = 0; i < ctx->num_peers; i++) { + if (strcmp(ctx->peers[i].hostname, pkt.hostname) == 0) { + already_exists = true; + break; + } + } + + if (!already_exists && ctx->num_peers < MAX_PEERS) { + peer_t *peer = &ctx->peers[ctx->num_peers]; + memset(peer, 0, sizeof(peer_t)); + + /* Parse address */ + struct sockaddr_in *addr = (struct sockaddr_in*)&peer->addr; + addr->sin_family = AF_INET; + addr->sin_port = pkt.listen_port; /* Already in network byte order */ + memcpy(&addr->sin_addr, &from_addr.sin_addr, sizeof(struct in_addr)); + peer->addr_len = sizeof(struct sockaddr_in); + + /* Store peer info */ + strncpy(peer->hostname, pkt.hostname, sizeof(peer->hostname) - 1); + peer->hostname[sizeof(peer->hostname) - 1] = '\0'; + + strncpy(peer->rootstream_code, pkt.rootstream_code, + sizeof(peer->rootstream_code) - 1); + peer->rootstream_code[sizeof(peer->rootstream_code) - 1] = '\0'; + + peer->state = PEER_DISCOVERED; + peer->last_seen = get_timestamp_ms(); + + ctx->num_peers++; + + printf(" → Added peer: %s (code: %.16s...)\n", + peer->hostname, peer->rootstream_code); + + /* Save to history */ + discovery_save_peer_to_history(ctx, pkt.hostname, ntohs(pkt.listen_port), + pkt.rootstream_code); + } + + return 1; /* Found peer */ +} diff --git a/src/discovery_manual.c b/src/discovery_manual.c new file mode 100644 index 0000000..63d5c4a --- /dev/null +++ b/src/discovery_manual.c @@ -0,0 +1,215 @@ +/* + * discovery_manual.c - Manual peer entry system + * + * Allows user to manually specify peer address or RootStream code. + * Always available as ultimate fallback. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include +#include + +/* + * Parse RootStream code (uppercase alphanumeric) + * Example: "ABCD-1234-EFGH-5678" or full format like "key@hostname" + */ +int discovery_parse_rootstream_code(rootstream_ctx_t *ctx, const char *code, + char *hostname, uint16_t *port) { + if (!ctx || !code || !hostname || !port) return -1; + + printf("INFO: Attempting to resolve RootStream code: %.32s...\n", code); + + /* Check peer history/favorites */ + for (int i = 0; i < ctx->num_peer_history; i++) { + if (strcmp(ctx->peer_history_entries[i].rootstream_code, code) == 0) { + strncpy(hostname, ctx->peer_history_entries[i].hostname, 255); + hostname[255] = '\0'; + *port = ctx->peer_history_entries[i].port; + printf("✓ Found code in history: %s:%u\n", hostname, *port); + return 0; + } + } + + fprintf(stderr, "ERROR: RootStream code not found in history\n"); + fprintf(stderr, "INFO: Use --peer-add to add a known peer\n"); + return -1; +} + +/* + * Parse IP:port address + * Examples: "192.168.1.100:5500" or "example.com:5500" + */ +int discovery_parse_address(const char *address, char *hostname, uint16_t *port) { + if (!address || !hostname || !port) return -1; + + char buf[256]; + strncpy(buf, address, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + /* Find last colon for port */ + char *colon = strrchr(buf, ':'); + if (!colon) { + fprintf(stderr, "ERROR: Address must be in format: hostname:port\n"); + return -1; + } + + *colon = '\0'; + + /* Copy hostname safely (assume 256 byte destination) */ + strncpy(hostname, buf, 255); + hostname[255] = '\0'; + + char *port_str = colon + 1; + int port_num = atoi(port_str); + if (port_num <= 0 || port_num > 65535) { + fprintf(stderr, "ERROR: Invalid port number: %s\n", port_str); + return -1; + } + + *port = (uint16_t)port_num; + printf("✓ Parsed address: %s:%u\n", hostname, *port); + return 0; +} + +/* + * Connect to manually specified peer + */ +int discovery_manual_add_peer(rootstream_ctx_t *ctx, const char *address_or_code) { + if (!ctx || !address_or_code) return -1; + + char hostname[256]; + uint16_t port = 9876; /* Default port */ + int ret = -1; + + /* Try to parse as IP:port first */ + if (strchr(address_or_code, ':')) { + ret = discovery_parse_address(address_or_code, hostname, &port); + } else { + /* Try as RootStream code */ + ret = discovery_parse_rootstream_code(ctx, address_or_code, hostname, &port); + } + + if (ret < 0) { + fprintf(stderr, "ERROR: Cannot parse peer address or code\n"); + return -1; + } + + /* Check if peer already exists */ + for (int i = 0; i < ctx->num_peers; i++) { + if (strcmp(ctx->peers[i].hostname, hostname) == 0) { + printf("INFO: Peer %s already exists\n", hostname); + return 0; + } + } + + /* Add peer */ + if (ctx->num_peers >= MAX_PEERS) { + fprintf(stderr, "ERROR: Maximum number of peers reached\n"); + return -1; + } + + printf("INFO: Manually connecting to %s:%u\n", hostname, port); + + peer_t *peer = &ctx->peers[ctx->num_peers]; + memset(peer, 0, sizeof(peer_t)); + + strncpy(peer->hostname, hostname, sizeof(peer->hostname) - 1); + peer->hostname[sizeof(peer->hostname) - 1] = '\0'; + peer->addr_len = sizeof(struct sockaddr_in); + + struct sockaddr_in *addr = (struct sockaddr_in *)&peer->addr; + addr->sin_family = AF_INET; + addr->sin_port = htons(port); + + /* Try to resolve hostname */ + if (inet_aton(hostname, &addr->sin_addr) == 0) { + /* Not an IP, try DNS resolution */ + struct hostent *h = gethostbyname(hostname); + if (!h) { + fprintf(stderr, "ERROR: Cannot resolve hostname: %s\n", hostname); + return -1; + } + memcpy(&addr->sin_addr, h->h_addr, h->h_length); + } + + peer->state = PEER_DISCOVERED; + peer->last_seen = get_timestamp_ms(); + + ctx->num_peers++; + + printf("✓ Manually added peer: %s (%s:%u)\n", hostname, hostname, port); + + /* Save to history */ + discovery_save_peer_to_history(ctx, hostname, port, address_or_code); + + return 0; +} + +/* + * Save peer to history for quick reconnect + */ +int discovery_save_peer_to_history(rootstream_ctx_t *ctx, const char *hostname, + uint16_t port, const char *rootstream_code) { + if (!ctx || !hostname) return -1; + + /* Check if already in history */ + for (int i = 0; i < ctx->num_peer_history; i++) { + if (strcmp(ctx->peer_history_entries[i].hostname, hostname) == 0 && + ctx->peer_history_entries[i].port == port) { + return 0; /* Already saved */ + } + } + + /* Add to history */ + if (ctx->num_peer_history >= MAX_PEER_HISTORY) { + /* Shift out oldest */ + memmove(&ctx->peer_history_entries[0], &ctx->peer_history_entries[1], + (MAX_PEER_HISTORY - 1) * sizeof(ctx->peer_history_entries[0])); + ctx->num_peer_history--; + } + + peer_history_entry_t *entry = &ctx->peer_history_entries[ctx->num_peer_history]; + memset(entry, 0, sizeof(peer_history_entry_t)); + + strncpy(entry->hostname, hostname, sizeof(entry->hostname) - 1); + entry->hostname[sizeof(entry->hostname) - 1] = '\0'; + + snprintf(entry->address, sizeof(entry->address), "%s:%u", hostname, port); + + entry->port = port; + + if (rootstream_code) { + strncpy(entry->rootstream_code, rootstream_code, + sizeof(entry->rootstream_code) - 1); + entry->rootstream_code[sizeof(entry->rootstream_code) - 1] = '\0'; + } + + ctx->num_peer_history++; + printf("✓ Saved peer to history\n"); + return 0; +} + +/* + * List saved peer history + */ +void discovery_list_peer_history(rootstream_ctx_t *ctx) { + if (!ctx || ctx->num_peer_history == 0) { + printf("No saved peers\n"); + return; + } + + printf("\nSaved Peers:\n"); + for (int i = 0; i < ctx->num_peer_history; i++) { + printf(" %d. %s (%s)\n", i + 1, + ctx->peer_history_entries[i].hostname, + ctx->peer_history_entries[i].address); + if (strlen(ctx->peer_history_entries[i].rootstream_code) > 0) { + printf(" Code: %.32s...\n", + ctx->peer_history_entries[i].rootstream_code); + } + } + printf("\n"); +} diff --git a/src/main.c b/src/main.c index a04067e..5e380ba 100644 --- a/src/main.c +++ b/src/main.c @@ -60,11 +60,18 @@ static void print_usage(const char *progname) { printf(" --latency-log Enable latency percentile logging\n"); printf(" --latency-interval MS Latency log interval in ms (default: 1000)\n"); printf("\n"); + printf("Manual Peer Entry (PHASE 5):\n"); + printf(" --peer-add IP:PORT Manually add peer by IP address and port\n"); + printf(" --peer-code CODE Connect using RootStream code from history\n"); + printf(" --peer-list List saved peer history\n"); + printf("\n"); printf("Examples:\n"); printf(" %s # Start tray app\n", progname); printf(" %s --qr # Show your code\n", progname); printf(" %s connect kXx7Y...@gaming-pc # Connect to peer\n", progname); printf(" %s host --display 1 --bitrate 15000 # Host on 2nd display\n", progname); + printf(" %s --peer-add 192.168.1.100:9876 # Manually add peer\n", progname); + printf(" %s --peer-list # Show saved peers\n", progname); printf("\n"); printf("First time setup:\n"); printf(" 1. Run 'rootstream --qr' to get your code\n"); @@ -286,6 +293,9 @@ int main(int argc, char **argv) { {"latency-log", no_argument, 0, 'l'}, {"latency-interval", required_argument, 0, 'i'}, {"backend-verbose", no_argument, 0, 0}, + {"peer-add", required_argument, 0, 0}, + {"peer-list", no_argument, 0, 0}, + {"peer-code", required_argument, 0, 0}, {0, 0, 0, 0} }; @@ -293,6 +303,9 @@ int main(int argc, char **argv) { bool list_displays = false; bool service_mode = false; bool no_discovery = false; + bool show_peer_list = false; + const char *peer_add = NULL; + const char *peer_code = NULL; uint16_t port = 9876; int display_idx = -1; int bitrate = 10000; @@ -310,6 +323,12 @@ int main(int argc, char **argv) { if (strcmp(long_options[option_index].name, "backend-verbose") == 0) { backend_verbose = true; printf("INFO: Backend selection verbose mode enabled\n"); + } else if (strcmp(long_options[option_index].name, "peer-add") == 0) { + peer_add = optarg; + } else if (strcmp(long_options[option_index].name, "peer-list") == 0) { + show_peer_list = true; + } else if (strcmp(long_options[option_index].name, "peer-code") == 0) { + peer_code = optarg; } break; case 'h': @@ -437,6 +456,40 @@ int main(int argc, char **argv) { printf("QR code saved to: %s\n", qr_path); } + printf("\nYour RootStream code:\n"); + printf(" %s\n\n", ctx.keypair.rootstream_code); + + rootstream_cleanup(&ctx); + return 0; + } + + /* Handle --peer-list flag (PHASE 5) */ + if (show_peer_list) { + discovery_list_peer_history(&ctx); + rootstream_cleanup(&ctx); + return 0; + } + + /* Handle --peer-add flag (PHASE 5) */ + if (peer_add) { + if (discovery_manual_add_peer(&ctx, peer_add) < 0) { + fprintf(stderr, "ERROR: Failed to add peer\n"); + rootstream_cleanup(&ctx); + return 1; + } + printf("INFO: Peer added successfully\n"); + rootstream_cleanup(&ctx); + return 0; + } + + /* Handle --peer-code flag (PHASE 5) */ + if (peer_code) { + if (discovery_manual_add_peer(&ctx, peer_code) < 0) { + fprintf(stderr, "ERROR: Failed to connect to peer\n"); + rootstream_cleanup(&ctx); + return 1; + } + printf("INFO: Peer connection initiated\n"); rootstream_cleanup(&ctx); return 0; } diff --git a/src/network.c b/src/network.c index 88f7cff..b938404 100644 --- a/src/network.c +++ b/src/network.c @@ -379,6 +379,8 @@ int rootstream_net_recv(rootstream_ctx_t *ctx, int timeout_ms) { fprintf(stderr, "ERROR: Invalid context\n"); return -1; } + + (void)timeout_ms; /* Reserved for future use */ /* First, check for reconnecting peers (iterate backwards to handle removal safely) */ for (int i = ctx->num_peers - 1; i >= 0; i--) { diff --git a/src/x11_capture.c b/src/x11_capture.c index 8d78a63..db51514 100644 --- a/src/x11_capture.c +++ b/src/x11_capture.c @@ -170,7 +170,7 @@ void rootstream_capture_cleanup_x11(rootstream_ctx_t *ctx) { /* Stub implementation when X11 is not available */ -static char last_error[256] = "X11 support not compiled in"; +static char last_error[256] __attribute__((unused)) = "X11 support not compiled in"; int rootstream_capture_init_x11(rootstream_ctx_t *ctx) { (void)ctx;