diff --git a/pixelpilot.yaml b/pixelpilot.yaml index 105f572..59f675f 100644 --- a/pixelpilot.yaml +++ b/pixelpilot.yaml @@ -59,3 +59,9 @@ os_sensors: # cpu: auto # power: auto # temperature: auto + +# Phone restream target IP (optional) +# manual_ip: permanently pins this IP in the "Stream To" dropdown so it is +# always visible as an option, even when auto-discovery finds no clients. +# restream: +# manual_ip: 192.168.1.100 diff --git a/src/gsmenu/gs_wifi.c b/src/gsmenu/gs_wifi.c index 10143e4..52d7c45 100644 --- a/src/gsmenu/gs_wifi.c +++ b/src/gsmenu/gs_wifi.c @@ -4,6 +4,7 @@ #include #include "ui.h" #include "../input.h" +#include "../gstrtpreceiver.h" #include "helper.h" #include "styles.h" #include "executor.h" @@ -23,6 +24,30 @@ lv_obj_t * password; lv_obj_t * wlan; lv_obj_t * hotspot; lv_obj_t * ipinfo; +lv_obj_t * restream; +lv_obj_t * ip_dropdown; + +static uint16_t find_dropdown_option_index(const char * options, const char * value) +{ + if (!value || value[0] == '\0') { + return 0; + } + + uint16_t idx = 0; + const char * option = options; + + while (option && *option) { + const char * nl = strchr(option, '\n'); + size_t len = nl ? (size_t)(nl - option) : strlen(option); + if (strlen(value) == len && strncmp(option, value, len) == 0) { + return idx; + } + idx++; + option = nl ? nl + 1 : NULL; + } + + return 0; +} void wifi_page_load_callback(lv_obj_t * page) { @@ -31,6 +56,31 @@ void wifi_page_load_callback(lv_obj_t * page) reload_textarea_value(page,ssid); reload_textarea_value(page,password); reload_label_value(page,ipinfo); + + if (restream_get_enabled()) lv_obj_add_state(lv_obj_get_child_by_type(restream,0,&lv_switch_class), LV_STATE_CHECKED); + else lv_obj_clear_state(lv_obj_get_child_by_type(restream,0,&lv_switch_class), LV_STATE_CHECKED); + { + char clients[512]; + restream_scan_clients(clients, sizeof(clients)); + lv_dropdown_set_options(ip_dropdown, clients); + lv_dropdown_set_selected(ip_dropdown, find_dropdown_option_index(clients, restream_get_manual_ip())); + } +} + +static void ip_dropdown_cb(lv_event_t * e) { + if (lv_event_get_code(e) == LV_EVENT_VALUE_CHANGED) { + lv_obj_t * dd = lv_event_get_target(e); + char buf[64]; + lv_dropdown_get_selected_str(dd, buf, sizeof(buf)); + restream_set_manual_ip(buf); + } +} + +static void restream_switch_callback(lv_event_t * e) { + if (lv_event_get_code(e) == LV_EVENT_VALUE_CHANGED) { + lv_obj_t * target = lv_event_get_target(e); + restream_set_enabled(lv_obj_has_state(target, LV_STATE_CHECKED)); + } } static void btn_event_cb(lv_event_t * e) @@ -138,6 +188,38 @@ void create_wifi_menu(lv_obj_t * parent) { lv_obj_add_event_cb(kb, kb_event_cb, LV_EVENT_ALL,kb); lv_keyboard_set_textarea(kb, NULL); + create_text(parent, NULL, "Phone Restream", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); + section = lv_menu_section_create(parent); + lv_obj_add_style(section, &style_openipc_section, 0); + + cont = lv_menu_cont_create(section); + lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); + + restream = create_switch(cont,LV_SYMBOL_VIDEO,"Phone Restream",NULL, NULL,false); + lv_obj_add_event_cb(lv_obj_get_child_by_type(restream,0,&lv_switch_class), restream_switch_callback, LV_EVENT_VALUE_CHANGED,NULL); + + lv_obj_t * ip_dropdown_row = lv_menu_cont_create(cont); + lv_obj_t * dd_icon = lv_image_create(ip_dropdown_row); + lv_image_set_src(dd_icon, LV_SYMBOL_WIFI); + lv_obj_t * dd_label = lv_label_create(ip_dropdown_row); + lv_label_set_text(dd_label, "Stream To"); + lv_obj_set_flex_grow(dd_label, 1); + ip_dropdown = lv_dropdown_create(ip_dropdown_row); + lv_dropdown_set_options(ip_dropdown, "Auto"); + lv_dropdown_set_dir(ip_dropdown, LV_DIR_RIGHT); + lv_dropdown_set_symbol(ip_dropdown, LV_SYMBOL_RIGHT); + lv_obj_set_width(ip_dropdown, 200); + lv_obj_add_style(ip_dropdown, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_obj_add_style(ip_dropdown, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_t * dd_list = lv_dropdown_get_list(ip_dropdown); + lv_obj_add_style(dd_list, &style_openipc, LV_PART_SELECTED | LV_STATE_CHECKED); + lv_obj_add_style(dd_list, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_event_cb(ip_dropdown, dropdown_event_handler, LV_EVENT_ALL, NULL); + lv_obj_add_event_cb(ip_dropdown, ip_dropdown_cb, LV_EVENT_VALUE_CHANGED, NULL); + lv_obj_add_event_cb(ip_dropdown, generic_back_event_handler, LV_EVENT_KEY, NULL); + lv_obj_add_event_cb(ip_dropdown, on_focus, LV_EVENT_FOCUSED, NULL); + lv_group_add_obj(menu_page_data->indev_group, ip_dropdown); + create_text(parent, NULL, "Network", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); section = lv_menu_section_create(parent); lv_obj_add_style(section, &style_openipc_section, 0); diff --git a/src/gstrtpreceiver.cpp b/src/gstrtpreceiver.cpp index 6609ee2..c136f9a 100644 --- a/src/gstrtpreceiver.cpp +++ b/src/gstrtpreceiver.cpp @@ -12,6 +12,7 @@ #include "spdlog/spdlog.h" #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +102,14 @@ namespace { static int g_idr_sock = -1; static std::atomic g_idr_sock_ready{false}; + static std::mutex g_restream_mutex; + static GstElement* g_restream_valve = nullptr; + static GstElement* g_restream_sink = nullptr; + static std::atomic g_restream_enabled{false}; + static std::string g_restream_target_ip; + static std::string g_restream_manual_ip; // user's active selection; empty = auto-discover + static std::string g_restream_pinned_ip; // always shown in dropdown, set from config + static std::mutex g_last_hop_mutex; static std::string g_last_hop_ip; static std::atomic g_last_pkt_ms{0}; @@ -122,6 +132,11 @@ namespace { } static void request_idr_bursts(const char* reason, int request_count, bool allow_pending); + static void maybe_update_restream_target(bool force); + + static bool contains_ip(const std::vector& ips, const std::string& ip) { + return !ip.empty() && std::find(ips.begin(), ips.end(), ip) != ips.end(); + } static bool is_stream_idr_reason(const char* reason) { return reason && !strcmp(reason, "stream-up"); @@ -152,6 +167,150 @@ namespace { return true; } + static void set_restream_valve_locked(bool enabled) { + if (!g_restream_valve) { + return; + } + g_object_set(G_OBJECT(g_restream_valve), "drop", enabled ? FALSE : TRUE, NULL); + } + + static void update_restream_valve(bool enabled) { + std::lock_guard lock(g_restream_mutex); + // Only force-close when disabling. Opening is handled by maybe_update_restream_target + // once a valid target IP is confirmed, to avoid briefly streaming to 127.0.0.1. + if (!enabled) { + set_restream_valve_locked(false); + } + } + + static void clear_restream_valve() { + std::lock_guard lock(g_restream_mutex); + if (!g_restream_valve) { + if (!g_restream_sink) { + return; + } + } + if (g_restream_valve) { + gst_object_unref(g_restream_valve); + g_restream_valve = nullptr; + } + if (g_restream_sink) { + gst_object_unref(g_restream_sink); + g_restream_sink = nullptr; + } + g_restream_target_ip.clear(); + } + + static void bind_restream_valve(GstElement* pipeline) { + clear_restream_valve(); + if (!pipeline || !GST_IS_BIN(pipeline)) { + return; + } + + GstElement* valve = gst_bin_get_by_name(GST_BIN(pipeline), "restream_valve"); + if (!valve) { + return; + } + + GstElement* sink = gst_bin_get_by_name(GST_BIN(pipeline), "restream_sink"); + if (!sink) { + gst_object_unref(valve); + return; + } + + { + std::lock_guard lock(g_restream_mutex); + g_restream_valve = valve; + g_restream_sink = sink; + g_restream_target_ip.clear(); + set_restream_valve_locked(false); + } + + maybe_update_restream_target(true); + } + + static std::string create_restream_branch() { + std::stringstream ss; + ss << " rtp_tee. ! valve name=restream_valve drop=true" + " ! queue leaky=downstream max-size-buffers=0 max-size-bytes=0 max-size-time=1000000000 silent=true" + " ! udpsink name=restream_sink host=0.0.0.0 port=5600 sync=false async=false qos=false"; + return ss.str(); + } + + static std::vector scan_hotspot_clients() { + std::ifstream arp_file("/proc/net/arp"); + if (!arp_file.is_open()) return {}; + std::string line; + std::getline(arp_file, line); // skip header + std::vector result; + while (std::getline(arp_file, line)) { + std::istringstream iss(line); + std::string ip, hw_type, flags, hw_address, mask, device; + if (!(iss >> ip >> hw_type >> flags >> hw_address >> mask >> device)) continue; + if (device != "wlan0" && device != "usb0") continue; + if (flags == "0x0" || hw_address == "00:00:00:00:00:00") continue; + result.push_back(ip); + } + return result; + } + + static std::string find_first_hotspot_client_ip() { + const auto clients = scan_hotspot_clients(); + if (contains_ip(clients, g_restream_target_ip)) { + return g_restream_target_ip; + } + return clients.empty() ? "" : clients.front(); + } + + static void maybe_update_restream_target(bool force) { + static uint64_t last_probe_ms = 0; + const uint64_t now = now_ms(); + if (!force && (now - last_probe_ms) < 1000) { + return; + } + last_probe_ms = now; + + bool new_target = false; + { + std::lock_guard lock(g_restream_mutex); + if (!g_restream_valve || !g_restream_sink) { + return; + } + if (!g_restream_enabled.load(std::memory_order_relaxed)) { + set_restream_valve_locked(false); + return; + } + + // If the user picked a specific IP use it, otherwise auto-discover. + const std::string next_ip = !g_restream_manual_ip.empty() + ? g_restream_manual_ip + : find_first_hotspot_client_ip(); + if (next_ip.empty()) { + if (!g_restream_target_ip.empty()) { + spdlog::info("[RESTREAM] No target client found; stopping unicast restream"); + g_restream_target_ip.clear(); + } + set_restream_valve_locked(false); + return; + } + + if (next_ip != g_restream_target_ip) { + g_restream_target_ip = next_ip; + g_object_set(G_OBJECT(g_restream_sink), "host", g_restream_target_ip.c_str(), NULL); + spdlog::info("[RESTREAM] Streaming to {}:{}", + g_restream_target_ip, + 5600); + new_target = true; + } + + set_restream_valve_locked(true); + } + + if (new_target) { + request_idr_bursts("restream-start", kIdrRepeatCount, false); + } + } + static uint32_t secure_random_u32() { uint32_t out = 0; #if defined(__linux__) @@ -753,6 +912,7 @@ static void loop_pull_appsink_samples(bool& keep_looping,GstElement *app_sink_el } gst_sample_unref(sample); } + maybe_update_restream_target(false); tick_stream_presence(); } } @@ -762,13 +922,15 @@ std::string GstRtpReceiver::construct_gstreamer_pipeline() { std::stringstream ss; if (! unix_socket) - ss<<"udpsrc port="< lock(g_restream_mutex); + manual_ip = g_restream_manual_ip; + pinned_ip = g_restream_pinned_ip; + } + + std::string combined = "Auto"; + for (const auto& ip : ips) { + combined += '\n'; + combined += ip; + } + // Always include the pinned IP from config so it stays in the list + // regardless of whether the user currently has it selected. + if (!pinned_ip.empty() && !contains_ip(ips, pinned_ip)) { + combined += '\n'; + combined += pinned_ip; + } + // Also include the active manual IP if it differs from the pinned/default ones. + if (!manual_ip.empty() && manual_ip != "Auto" && manual_ip != pinned_ip + && !contains_ip(ips, manual_ip)) { + combined += '\n'; + combined += manual_ip; + } + strncpy(buf, combined.c_str(), buf_len - 1); + buf[buf_len - 1] = '\0'; +} + +void restream_set_manual_ip(const char* ip) { + std::lock_guard lock(g_restream_mutex); + g_restream_manual_ip = (ip && ip[0] != '\0' && strcmp(ip, "Auto") != 0) ? ip : ""; + g_restream_target_ip.clear(); // force retarget on next probe +} + +void restream_set_pinned_ip(const char* ip) { + std::lock_guard lock(g_restream_mutex); + g_restream_pinned_ip = (ip && ip[0] != '\0') ? ip : ""; +} + +const char* restream_get_manual_ip() { + std::lock_guard lock(g_restream_mutex); + static char buf[64]; + strncpy(buf, g_restream_manual_ip.c_str(), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return buf; +} + void idr_request_record_start() { request_idr_bursts("record-start", kIdrRecordRepeatCount, true); } diff --git a/src/gstrtpreceiver.h b/src/gstrtpreceiver.h index b5b0a0f..18bed5e 100644 --- a/src/gstrtpreceiver.h +++ b/src/gstrtpreceiver.h @@ -94,6 +94,12 @@ extern "C" { #endif void idr_set_enabled(bool enabled); bool idr_get_enabled(); +void restream_set_enabled(bool enabled); +bool restream_get_enabled(); +void restream_scan_clients(char* buf, size_t buf_len); +void restream_set_manual_ip(const char* ip); +const char* restream_get_manual_ip(); +void restream_set_pinned_ip(const char* ip); void idr_request_record_start(); void idr_request_decoder_issue(const char* reason); void idr_notify_decoded_frame(); diff --git a/src/main.cpp b/src/main.cpp index 3b990a0..1be5d02 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1014,6 +1014,11 @@ int main(int argc, char **argv) } } + if (config["restream"] && config["restream"]["manual_ip"]) { + std::string ip = config["restream"]["manual_ip"].as(); + restream_set_pinned_ip(ip.c_str()); + } + if (config["os_sensors"] && config["os_sensors"].IsMap()) { if (config["os_sensors"]["cpu"]) { auto cpu = config["os_sensors"]["cpu"];