#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dma_util.h" #include "decoder.h" #include "composer.h" #include "display.h" #include "plc.h" #include "title.h" #include "flog.h" #include "hikvision.h" #include "web_server.h" #define MAX_STREAMS 25 #define MAX_TILES 25 #define MAX_ISAPI_WRITE 64 #define MAX_LAYOUTS 16 #define MAX_RULES 32 #define MAX_RULE_BITS 16 /* Split layout mode: fixed top row + PLC-driven adaptive bottom row. */ #define SPLIT_MAX_BOTTOM 8 static int g_split_enabled = 0; /* 1 = use split mode */ static int g_split_top_ids[MAX_TILES]; /* top row camera ids */ static int g_split_top_n = 0; /* top row count */ static double g_split_top_ratio = 0.3; /* top row height fraction */ static int g_split_bottom_ids[SPLIT_MAX_BOTTOM][MAX_TILES]; /* per variant */ static int g_split_bottom_cnt[SPLIT_MAX_BOTTOM] = {0}; /* cams per variant */ static int g_split_bottom_valid[SPLIT_MAX_BOTTOM] = {0}; /* 1 if variant usable */ static int g_split_bottom_count = 0; /* temp: current bottom_N being parsed */ static int g_split_bottom_def_ids[MAX_TILES] = {0}; /* [layout_bottom] default */ static int g_split_bottom_def_cnt = 0; static int g_split_bottom_def_valid = 0; /* Default bottom layout used when no PLC rule matches: the [layout_bottom] * group when present, else the highest valid [layout_bottom_N]. Sentinel 0 * means "use the [layout_bottom] default group". */ static int split_idle_bottom(void) { if (g_split_bottom_def_valid) return 0; for (int bv = SPLIT_MAX_BOTTOM; bv >= 1; bv--) if (g_split_bottom_valid[bv - 1]) return bv; return 1; } static int split_bottom_cfg(int n, int **ids) { if (n <= 0) { *ids = g_split_bottom_def_ids; return g_split_bottom_def_cnt; } *ids = g_split_bottom_ids[n - 1]; return g_split_bottom_cnt[n - 1]; } /* 超过该时长没有任何新帧到达,视为流静默死亡,worker 断开并重连。 */ #define STREAM_STALL_TIMEOUT_MS 5000 /* 最后一次新帧后超过该时长仍无数据,主循环停止定格旧帧并显示 NO SIGNAL。 */ #define HOLD_LAST_FRAME_MS 8000 typedef struct { char url[512]; char url_main[512]; /* main-stream URL; used when a single-channel * layout is active (use_main=1). Empty = none. */ Decoder *dec; int stream_id; int running; pthread_t thread; int connected; int codec; int width; int height; /* Ring buffer: worker writes, main reads */ DecodedFrame ring[2]; int ring_count; int ring_head; pthread_mutex_t mutex; pthread_cond_t cond; /* Worker-assigned monotonic frame seq (stored in ring[].pts) */ int64_t frame_seq; /* Main currently holds this decoder buffer index (set by main while it * composes/displays the frame; worker must not reuse it). -1 = none. */ int held_pin; /* Main/sub stream selection (guarded by mutex). Main thread flips * use_main when the active layout is single-channel; the worker * reconnects to url_main/url accordingly and records which one it * is currently connected to in dec_using_main. */ int use_main; int dec_using_main; /* Dup'd fd of the last frame main composed from this stream. Kept across * reconnects/layout switches so a brief gap shows the frozen last frame * instead of NO SIGNAL. -1 = never had a frame. (main thread only) */ int last_fd; int last_w, last_h, last_stride, last_vstride; /* (main thread only) Monotonic time of the last *new* frame and the seq * it was stamped with, so a silent stream is detected even though the * same ring buffer keeps being re-acquired. */ int64_t last_seq_seen; double last_frame_mono; } StreamWorker; static StreamWorker workers[MAX_STREAMS]; static int num_streams = 0; static int tile_map[MAX_TILES]; static int num_tiles = 0; static volatile sig_atomic_t g_running = 1; static volatile sig_atomic_t g_rebuild_req = 0; static Composer *g_composer = NULL; static Display *g_display = NULL; static int display_fail_streak = 0; /* consecutive display_commit failures */ static int output_fd[3] = {-1, -1, -1}; static void *output_map[3] = {NULL, NULL, NULL}; static int output_front = 0; static int output_last = 0; /* previous front, for triple-buffer reuse */ static int output_w = 0; static int output_h = 0; static int output_stride = 0; static size_t output_size = 0; /* Triple buffering: pick a back buffer that is neither the current scanout * (front) nor the immediately previous one (last), so VOP can never be mid-scan * on a buffer we are about to overwrite -> eliminates tearing. */ #define BACK_BUF() (3 - output_front - output_last) /* ---- HikVision ISAPI camera list / optional writes ---- */ typedef struct { int id; int main; /* 1 = main stream channel, 0 = sub */ int width, height, fps, bitrate; } IsapiWrite; static HvCam g_cams[MAX_STREAMS]; static HvUrls g_urls[MAX_STREAMS]; static int g_num_cams = 0; static IsapiWrite g_isapi_write[MAX_ISAPI_WRITE]; static int g_isapi_write_count = 0; /* ---- reconnect / display behaviour options ---- */ static int g_reconnect_ms = 3000;static int g_reconnect_max_ms = 15000; static int g_connect_timeout_sec = 10; static int g_letterbox = 1; static int g_no_signal_text = 1; static int g_blank_on_missing = 1; static int g_grid_lines = 1; /* draw grid separators between tiles (0 to disable) */ static int g_hold_layout_sec = 5; static double g_last_layout_ts = 0; /* Layout switch debounce: a new layout must stay stable for this many ms * before it is applied, so a momentary PLC bit pulse (a point that triggers * and clears within one scan) does not visibly flicker the screen. */ static int g_layout_debounce_ms = 300; static int g_pending_layout = -1; static double g_pending_layout_ts = 0; /* ---- PLC-driven layout state ---- */ typedef struct { int id; int map[MAX_TILES]; /* cam ids in grid order; -1 = empty cell */ int n; /* total cells across all rows */ int row_cols[MAX_TILES]; /* column count per row (row-major) */ int nrows; /* number of rows */ } LayoutDef; typedef struct { int byte_idx; /* offset relative to plc read_start */ int bit; } RuleBit; typedef struct { int layout; /* index into g_layouts */ RuleBit bits[MAX_RULE_BITS]; int group[MAX_RULE_BITS]; /* group index per bit (0-based) */ int nbits; int ngroups; /* number of ';' groups */ int all; /* 1 = all bits in a group required, 0 = any */ } PlcRule; static LayoutDef g_layouts[MAX_LAYOUTS]; static int g_num_layouts = 0; static PlcRule g_rules[MAX_RULES]; static int g_num_rules = 0; static int g_plc_enabled = 0; static PlcConfig g_plc_cfg; /* [auto_reboot] scheduled device restart */ static int g_reboot_enable = 0; /* 0=off 1=on */ static int g_reboot_period = 0; /* 0=daily 1=weekly */ static int g_reboot_hour = 3; /* HH */ static int g_reboot_min = 0; /* MM */ static int g_reboot_wday = -1; /* tm_wday (0=Sun..6=Sat); -1 = any day */ static int g_cur_layout = -1; static int g_grid_x[MAX_TILES], g_grid_y[MAX_TILES]; static int g_grid_w[MAX_TILES], g_grid_h[MAX_TILES]; static int g_grid_nrows = 0; /* base [layout] grid: #rows */ static int g_grid_row_cols[MAX_TILES]; /* base grid: cols per row */ /* Active layout compiled to regions (merged cells). Each region maps 1:1 to a * camera and a screen rectangle; these drive composition every frame. */ static int g_disp_sid[MAX_TILES]; static int g_disp_tiles = 0; /* ---- title bar ---- */ static TitleCfg g_title; static int g_title_enabled = 0; static int g_title_h = 0; static int g_title_overlay = 0; /* ---- fault log ---- */ static char g_flog_path[512]; static long g_flog_rotate_ms = 3600000; static long g_flog_max_bytes = 1 << 20; static void sig_handler(int sig) { if (sig == SIGUSR1) { g_rebuild_req = 1; return; } g_running = 0; } static void install_signal_handlers(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; /* no SA_RESTART: block syscalls must return so loops see g_running */ sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGUSR1, &sa, NULL); } /* PLC 离线/恢复状态跳变时写一次故障日志(不在画面上叠加提示文字, * 避免画面右下角每帧闪烁)。 */ static void log_plc_state_change(void) { if (!g_plc_enabled) return; int offline = !plc_is_connected(); static int last_offline = -1; if (offline == last_offline) return; last_offline = offline; if (offline) flog("PLC 离线报警"); else flog("PLC 恢复在线"); } /* Current IPv4 address(es) shown on screen (eth0 first, then any other * non-loopback address), refreshed about once a second. Lets the operator * read the board's IP straight off the video if they forgot it. */ static char g_ip_text[128] = ""; static time_t g_ip_last = 0; static void update_ip_text(void) { time_t now = time(NULL); if (now == g_ip_last && g_ip_text[0]) return; g_ip_last = now; char eth0[48] = "", other[48] = "", extra[48] = ""; struct ifaddrs *ifa = NULL; if (getifaddrs(&ifa) == 0) { for (struct ifaddrs *p = ifa; p; p = p->ifa_next) { if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue; if (strcmp(p->ifa_name, "lo") == 0) continue; char buf[48]; struct sockaddr_in *sin = (struct sockaddr_in *)p->ifa_addr; if (!inet_ntop(AF_INET, &sin->sin_addr, buf, sizeof(buf))) continue; if (strcmp(p->ifa_name, "eth0") == 0) { if (!eth0[0]) snprintf(eth0, sizeof(eth0), "%s", buf); else if (!extra[0]) snprintf(extra, sizeof(extra), "%s", buf); } else if (!other[0]) { snprintf(other, sizeof(other), "%s", buf); } } freeifaddrs(ifa); } const char *a = eth0[0] ? eth0 : other; const char *b = eth0[0] ? extra : NULL; char line[128]; if (!a[0]) snprintf(line, sizeof(line), "IP 无地址"); else if (b && b[0]) snprintf(line, sizeof(line), "IP %s %s", a, b); else snprintf(line, sizeof(line), "IP %s", a); if (strcmp(line, g_ip_text) != 0) { snprintf(g_ip_text, sizeof(g_ip_text), "%s", line); fprintf(stderr, "IP 显示: %s\n", g_ip_text); } } /* Draw a 2px grid line around every on-screen tile in the NV12 output buffer. * Lines use a bright gray (Y high, neutral UV) so they read as white grid * separators over any video content. Call after composition, on the back * buffer (which the caller keeps CPU-visible). */ static void draw_grid_lines(uint8_t *map, int out_w, int out_h, Composer *comp, int n) { if (!map || !comp || n <= 0) return; static const int TH = 2; /* line thickness, px */ static const uint8_t YW = 220; /* near-white */ uint8_t *Y = map; for (int t = 0; t < n; t++) { int x, y, w, h; composer_get_tile_layout(comp, t, &x, &y, &w, &h); if (x < 0) x = 0; if (y < 0) y = 0; if (x + w > out_w) w = out_w - x; if (y + h > out_h) h = out_h - y; if (w <= 0 || h <= 0) continue; /* top */ for (int r = 0; r < TH && y + r < out_h; r++) memset(Y + (size_t)(y + r) * out_w + x, YW, (size_t)w); /* bottom */ int by = y + h - TH; if (by < 0) by = 0; for (int r = 0; r < TH && by + r < out_h; r++) memset(Y + (size_t)(by + r) * out_w + x, YW, (size_t)w); /* left */ for (int yy = y; yy < y + h && yy < out_h; yy++) { int x0 = x, x1 = x + TH; if (x1 > out_w) x1 = out_w; if (x0 < 0) x0 = 0; for (int xx = x0; xx < x1; xx++) Y[(size_t)yy * out_w + xx] = YW; } /* right */ int xr0 = x + w - TH; if (xr0 < 0) xr0 = 0; for (int yy = y; yy < y + h && yy < out_h; yy++) { int xx0 = xr0, xx1 = x + w; if (xx0 < 0) xx0 = 0; if (xx1 > out_w) xx1 = out_w; for (int xx = xx0; xx < xx1; xx++) Y[(size_t)yy * out_w + xx] = YW; } } } static void *worker_thread(void *arg) { StreamWorker *w = (StreamWorker *)arg; int backoff = g_reconnect_ms; struct timespec last_data_ts; while (g_running && w->running) { /* Pick the stream kind this worker should use right now. */ pthread_mutex_lock(&w->mutex); int want_main = w->use_main && w->url_main[0]; pthread_mutex_unlock(&w->mutex); if (!w->dec) { const char *url = want_main ? w->url_main : w->url; fprintf(stderr, "DBG: [stream %d] Connecting to %s\n", w->stream_id, url); w->dec = decoder_create(url, g_connect_timeout_sec); if (!w->dec) { fprintf(stderr, "DBG: [stream %d] Failed to connect\n", w->stream_id); flog("视频流 %d 连接失败: %s", w->stream_id, url); usleep((useconds_t)backoff * 1000); if (backoff < g_reconnect_max_ms) { backoff *= 2; if (backoff > g_reconnect_max_ms) backoff = g_reconnect_max_ms; } continue; } pthread_mutex_lock(&w->mutex); w->dec_using_main = want_main; w->connected = 1; pthread_mutex_unlock(&w->mutex); backoff = g_reconnect_ms; clock_gettime(CLOCK_MONOTONIC, &last_data_ts); w->codec = decoder_get_codec(w->dec); w->width = decoder_get_width(w->dec); w->height = decoder_get_height(w->dec); fprintf(stderr, "DBG: [stream %d] Connected: %dx%d codec=%d\n", w->stream_id, w->width, w->height, w->codec); flog("视频流 %d 已连接 %dx%d", w->stream_id, w->width, w->height); } while (g_running && w->running) { /* A single-channel layout switched which stream kind to present: * drop out and reconnect on the other URL. */ pthread_mutex_lock(&w->mutex); int want_now = w->use_main && w->url_main[0]; int switch_now = (want_now != w->dec_using_main); pthread_mutex_unlock(&w->mutex); if (switch_now) { fprintf(stderr, "DBG: [stream %d] switching to %s stream\n", w->stream_id, want_now ? "main" : "sub"); backoff = 50; break; } DecodedFrame frame; int ret = decoder_get_frame(w->dec, &frame); if (ret == -3) { /* No frame ready. A live stream delivers continuously, so a * long silent stretch means the connection died quietly * (RTSP server gone / TCP dropped without a FIN). */ struct timespec now_ts; clock_gettime(CLOCK_MONOTONIC, &now_ts); double idle = (now_ts.tv_sec - last_data_ts.tv_sec) + (now_ts.tv_nsec - last_data_ts.tv_nsec) / 1000000000.0; if (idle >= STREAM_STALL_TIMEOUT_MS / 1000.0) { flog_throttled("stream_stall", 10000, "视频流 %d 无数据 %d 秒,重连", w->stream_id, STREAM_STALL_TIMEOUT_MS / 1000); break; } usleep(1000); continue; } if (ret < 0) { flog_throttled("stream_break", 10000, "视频流 %d 解码中断 (ret=%d)", w->stream_id, ret); break; } clock_gettime(CLOCK_MONOTONIC, &last_data_ts); pthread_mutex_lock(&w->mutex); /* Never recycle the buffer main is currently displaying. */ while (w->ring_count >= 2 && g_running && w->running && w->ring[w->ring_head].idx == w->held_pin) { pthread_cond_wait(&w->cond, &w->mutex); } if (w->ring_count >= 2) { int ri = w->ring_head; decoder_release_frame(w->dec, w->ring[ri].idx); if (w->ring[ri].fd >= 0) close(w->ring[ri].fd); w->ring[ri].fd = -1; w->ring_head = (w->ring_head + 1) % 2; w->ring_count--; } int wi = (w->ring_head + w->ring_count) % 2; w->ring[wi] = frame; w->ring[wi].pts = ++w->frame_seq; w->ring_count++; pthread_cond_signal(&w->cond); pthread_mutex_unlock(&w->mutex); } /* Tear down decoder. Wait (bounded) for main to drain the ring so no * decoder buffer is outstanding when the pool is destroyed. */ pthread_mutex_lock(&w->mutex); w->connected = 0; if (w->dec) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 1; while (g_running && w->ring_count > 0) { int rc = pthread_cond_timedwait(&w->cond, &w->mutex, &ts); if (rc == ETIMEDOUT) break; } } Decoder *old = w->dec; w->dec = NULL; pthread_mutex_unlock(&w->mutex); decoder_destroy(old); fprintf(stderr, "DBG: [stream %d] decoder released\n", w->stream_id); if (!(g_running && w->running)) break; usleep((useconds_t)backoff * 1000); if (backoff < g_reconnect_max_ms) { backoff *= 2; if (backoff > g_reconnect_max_ms) backoff = g_reconnect_max_ms; } } /* final cleanup on shutdown */ pthread_mutex_lock(&w->mutex); Decoder *last = w->dec; w->dec = NULL; w->connected = 0; pthread_mutex_unlock(&w->mutex); decoder_destroy(last); printf("[stream %d] Exiting\n", w->stream_id); return NULL; } /* Parse one layout row into per-cell region labels. * - decimal number (0..25) -> that region/camera id * - single letter A-Z -> alias for region id (A=0, B=1, ... Z=25) * - x / X -> empty cell (-1) * Cells sharing the same label are merged into one on-screen region. */ static int parse_grid_row(char *s, int *out, int max) { int nv = 0; while (*s) { while (*s == ' ' || *s == '\t') s++; if (*s == 'x' || *s == 'X') { out[nv++] = -1; /* empty cell */ s++; } else if (*s >= '0' && *s <= '9') { out[nv++] = atoi(s); while (*s >= '0' && *s <= '9') s++; } else if ((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z')) { int c = *s; out[nv++] = (c >= 'a' && c <= 'z') ? (c - 'a') : (c - 'A'); s++; } else { break; } if (nv >= max) break; } return nv; } static int parse_config(const char *path, int *p_w, int *p_h, int *p_nstreams, int *p_ntiles, int *tilemap) { FILE *fp = fopen(path, "r"); if (!fp) return -1; char line[1024]; int section = 0; int nstreams = 0, ntiles = 0, ncols = 0; memset(&g_title, 0, sizeof(g_title)); g_plc_cfg.tsap_local = -1; g_plc_cfg.tsap_remote = -1; g_title.enable = 0; g_title.height = 80; g_title.font_size = 48; g_title.color = 0xFFFFFF; g_title.logo_pad = 8; g_title.opacity = 255; strncpy(g_title.font, title_default_font(), sizeof(g_title.font) - 1); g_flog_path[0] = '\0'; g_flog_rotate_ms = 3600000; g_flog_max_bytes = 1 << 20; while (fgets(line, sizeof(line), fp)) { char *s = line; while (*s == ' ' || *s == '\t') s++; if (*s == '#' || *s == '\n' || *s == '\r') continue; if (s[0] == '[') { if (strncmp(s, "[output]", 8) == 0) section = 1; else if (strncmp(s, "[streams]", 9) == 0) section = 2; else if (strncmp(s, "[main_streams]", 14) == 0) section = 11; else if (strncmp(s, "[camera_list]", 13) == 0) section = 12; else if (strncmp(s, "[isapi_write]", 13) == 0) section = 13; else if (strncmp(s, "[plc]", 5) == 0) section = 5; else if (strncmp(s, "[title]", 7) == 0) section = 9; else if (strncmp(s, "[log]", 5) == 0) section = 10; else if (strncmp(s, "[plc_rules]", 11) == 0) section = 7; else if (strncmp(s, "[auto_reboot]", 13) == 0) section = 17; else if (strncmp(s, "[layout", 7) == 0) { if (s[7] == ']') { section = 3; /* [layout] base grid */ } else if (s[7] >= '0' && s[7] <= '9') { section = 8; /* [layoutN] named grid */ if (g_num_layouts < MAX_LAYOUTS) { LayoutDef *L = &g_layouts[g_num_layouts]; memset(L, 0, sizeof(*L)); L->id = atoi(s + 7); g_num_layouts++; } } else if (strncmp(s + 7, "_top]", 5) == 0) { section = 14; /* [layout_top] */ } else if (strncmp(s + 7, "_bottom", 7) == 0) { if (s[14] == '_' && s[15] >= '0' && s[15] <= '9') { section = 15; /* [layout_bottom_N] */ g_split_bottom_count = atoi(s + 15); } else { section = 16; /* [layout_bottom] default row */ } } else { section = 0; } } else { section = 0; } continue; } if (section == 1) { if (strchr(s, '=')) { char key[64], val[64]; if (sscanf(s, "%63s = %63s", key, val) >= 2) { if (strcmp(key, "letterbox") == 0) g_letterbox = atoi(val) != 0; else if (strcmp(key, "no_signal_text") == 0) g_no_signal_text = atoi(val) != 0; else if (strcmp(key, "blank_on_missing") == 0) g_blank_on_missing = atoi(val) != 0; else if (strcmp(key, "grid_lines") == 0) g_grid_lines = atoi(val) != 0; } } else { sscanf(s, "%d %d", p_w, p_h); } } else if (section == 2) { if (strncmp(s, "reconnect_ms", 12) == 0 && strchr(s, '=')) { g_reconnect_ms = atoi(strchr(s, '=') + 1); } else if (strncmp(s, "reconnect_max_ms", 16) == 0 && strchr(s, '=')) { g_reconnect_max_ms = atoi(strchr(s, '=') + 1); } else if (strncmp(s, "connect_timeout_sec", 19) == 0 && strchr(s, '=')) { g_connect_timeout_sec = atoi(strchr(s, '=') + 1); } else { if (nstreams >= MAX_STREAMS) continue; char *url = strchr(s, ' '); if (!url) url = strchr(s, '\t'); if (!url) { url = s; } else { url++; } while (*url == ' ' || *url == '\t') url++; size_t len = strlen(url); while (len > 0 && (url[len-1] == '\n' || url[len-1] == '\r')) url[--len] = 0; if (len > 0) { size_t cp = len < sizeof(workers[nstreams].url) - 1 ? len : sizeof(workers[nstreams].url) - 1; memcpy(workers[nstreams].url, url, cp); workers[nstreams].url[cp] = '\0'; nstreams++; } } } else if (section == 3) { /* Check for key=value (split mode) */ if (strchr(s, '=')) { char key[64], val[64]; if (sscanf(s, "%63s = %63s", key, val) >= 2) { if (strcmp(key, "split") == 0) g_split_enabled = atoi(val) != 0; else if (strcmp(key, "top_ratio") == 0) g_split_top_ratio = atof(val); } } else { int vals[32]; int nv = parse_grid_row(s, vals, 32); if (nv > 0) { if (ncols == 0) ncols = nv; if (g_grid_nrows < MAX_TILES) { g_grid_row_cols[g_grid_nrows++] = nv; } for (int j = 0; j < nv && ntiles < MAX_TILES; j++) tilemap[ntiles++] = vals[j]; } } } else if (section == 14) { /* [layout_top]: space-separated camera IDs for top row */ int id; const char *p = s; while (*p && g_split_top_n < MAX_TILES) { while (*p == ' ' || *p == '\t') p++; if (!*p || *p == '\n' || *p == '\r' || *p == '#') break; if (sscanf(p, "%d", &id) == 1) { g_split_top_ids[g_split_top_n++] = id; while (*p && *p != ' ' && *p != '\t') p++; } else break; } } else if (section == 15) { /* [layout_bottom_N]: space-separated camera IDs for bottom row */ int id; const char *p = s; int cnt = 0; while (*p && cnt < MAX_TILES) { while (*p == ' ' || *p == '\t') p++; if (!*p || *p == '\n' || *p == '\r' || *p == '#') break; if (sscanf(p, "%d", &id) == 1) { if (g_split_bottom_count >= 1 && g_split_bottom_count <= SPLIT_MAX_BOTTOM) g_split_bottom_ids[g_split_bottom_count - 1][cnt] = id; cnt++; while (*p && *p != ' ' && *p != '\t') p++; } else break; } if (g_split_bottom_count >= 1 && g_split_bottom_count <= SPLIT_MAX_BOTTOM) { g_split_bottom_valid[g_split_bottom_count - 1] = 1; g_split_bottom_cnt[g_split_bottom_count - 1] = cnt; fprintf(stderr, "DBG: [layout_bottom_%d] cnt=%d ids=%d %d %d\n", g_split_bottom_count, cnt, g_split_bottom_ids[g_split_bottom_count-1][0], g_split_bottom_ids[g_split_bottom_count-1][1], g_split_bottom_ids[g_split_bottom_count-1][2]); } } else if (section == 16) { /* [layout_bottom]: default bottom row used when no PLC rule matches */ int id; const char *p = s; int cnt = 0; while (*p && cnt < MAX_TILES) { while (*p == ' ' || *p == '\t') p++; if (!*p || *p == '\n' || *p == '\r' || *p == '#') break; if (sscanf(p, "%d", &id) == 1) { g_split_bottom_def_ids[cnt++] = id; while (*p && *p != ' ' && *p != '\t') p++; } else break; } if (cnt > 0) { g_split_bottom_def_cnt = cnt; g_split_bottom_def_valid = 1; fprintf(stderr, "DBG: [layout_bottom] default cnt=%d ids=%d %d %d\n", cnt, g_split_bottom_def_ids[0], g_split_bottom_def_ids[1], g_split_bottom_def_ids[2]); } } else if (section == 8) { int vals[32]; int nv = parse_grid_row(s, vals, 32); if (nv > 0 && g_num_layouts > 0) { LayoutDef *L = &g_layouts[g_num_layouts - 1]; if (L->nrows < MAX_TILES) L->row_cols[L->nrows++] = nv; for (int j = 0; j < nv && L->n < MAX_TILES; j++) L->map[L->n++] = vals[j]; /* -1 = empty slot */ } } else if (section == 5) { char key[64], val[256]; if (sscanf(s, "%63s = %255[^\n\r]", key, val) >= 2) { char *c = strchr(val, ';'); if (!c) c = strchr(val, '#'); if (c) *c = '\0'; int vlen = (int)strlen(val); while (vlen > 0 && (val[vlen-1] == ' ' || val[vlen-1] == '\t')) val[--vlen] = '\0'; if (strcmp(key, "enable") == 0) { g_plc_enabled = atoi(val) != 0; } else if (strcmp(key, "ip") == 0) { strncpy(g_plc_cfg.ip, val, sizeof(g_plc_cfg.ip) - 1); } else if (strcmp(key, "port") == 0) { g_plc_cfg.port = atoi(val); } else if (strcmp(key, "area") == 0) { if (strcasecmp(val, "m") == 0) g_plc_cfg.area = PLC_AREA_M; else if (strcasecmp(val, "i") == 0) g_plc_cfg.area = PLC_AREA_I; else if (strcasecmp(val, "q") == 0) g_plc_cfg.area = PLC_AREA_Q; else if (strcasecmp(val, "db") == 0) g_plc_cfg.area = PLC_AREA_DB; } else if (strcmp(key, "db") == 0) { g_plc_cfg.db = atoi(val); } else if (strcmp(key, "read_start") == 0) { g_plc_cfg.read_start = atoi(val); } else if (strcmp(key, "read_len") == 0) { g_plc_cfg.read_len = atoi(val); } else if (strcmp(key, "poll_ms") == 0) { g_plc_cfg.poll_ms = atoi(val); } else if (strcmp(key, "reconnect_ms") == 0) { g_plc_cfg.reconnect_ms = atoi(val); } else if (strcmp(key, "hold_layout_sec") == 0) { g_hold_layout_sec = atoi(val); } else if (strcmp(key, "debounce_ms") == 0) { g_layout_debounce_ms = atoi(val); } else if (strcmp(key, "tsap_local") == 0) { g_plc_cfg.tsap_local = (int)strtol(val, NULL, 0); } else if (strcmp(key, "tsap_remote") == 0) { g_plc_cfg.tsap_remote = (int)strtol(val, NULL, 0); } } } else if (section == 7) { /* : .[,....] [';' ...] [: all] * ';' separates groups; every group must be satisfied (AND across * groups), any single bit inside a group is enough (OR within a * group); ':' suffix "all" also requires every bit of a group. */ if (g_num_rules >= MAX_RULES) continue; int lid; char *colon = strchr(s, ':'); if (!colon) continue; if (sscanf(s, "%d", &lid) != 1) continue; PlcRule *R = &g_rules[g_num_rules]; memset(R, 0, sizeof(*R)); char *tok = colon + 1; int cur_group = 0; while (tok && *tok && R->nbits < MAX_RULE_BITS) { while (*tok == ' ' || *tok == '\t' || *tok == ',' || *tok == ';') { if (*tok == ';') cur_group++; tok++; } if (*tok == ':' || *tok == '\0' || *tok == '\n' || *tok == '\r') break; int by, bi = 0; if (sscanf(tok, "%d.%d", &by, &bi) == 2) { R->bits[R->nbits].byte_idx = by; R->bits[R->nbits].bit = bi; } else if (sscanf(tok, "%d", &by) == 1) { R->bits[R->nbits].byte_idx = by; R->bits[R->nbits].bit = 0; } else { break; } R->group[R->nbits] = cur_group; R->nbits++; while (*tok && *tok != ',' && *tok != ';' && *tok != ':') tok++; } R->ngroups = cur_group + 1; char *mode = strrchr(s, ':'); if (mode && strstr(mode, "all")) R->all = 1; int li = -1; for (int i = 0; i < g_num_layouts; i++) if (g_layouts[i].id == lid) { li = i; break; } if (li < 0) continue; R->layout = li; if (R->nbits > 0) g_num_rules++; } else if (section == 9) { char key[64], val[512]; if (sscanf(s, "%63s = %511[^\n\r]", key, val) >= 2) { int vlen = (int)strlen(val); while (vlen > 0 && (val[vlen-1] == ' ' || val[vlen-1] == '\t' || val[vlen-1] == '\r' || val[vlen-1] == '\n')) val[--vlen] = '\0'; char *lead = val; while (*lead == ' ' || *lead == '\t') lead++; if (strcmp(key, "enable") == 0) { g_title.enable = atoi(lead) != 0; } else if (strcmp(key, "type") == 0) { if (strncmp(lead, "overlay", 7) == 0 || atoi(lead) != 0) g_title.type = 1; else g_title.type = 0; } else if (strcmp(key, "height") == 0) { g_title.height = atoi(lead); } else if (strcmp(key, "text") == 0) { strncpy(g_title.text, lead, sizeof(g_title.text) - 1); } else if (strcmp(key, "logo") == 0) { strncpy(g_title.logo, lead, sizeof(g_title.logo) - 1); } else if (strcmp(key, "logo_pad") == 0) { g_title.logo_pad = atoi(lead); } else if (strcmp(key, "opacity") == 0) { g_title.opacity = atoi(lead); } else if (strcmp(key, "font") == 0) { strncpy(g_title.font, lead, sizeof(g_title.font) - 1); } else if (strcmp(key, "font_size") == 0) { g_title.font_size = atoi(lead); } else if (strcmp(key, "color") == 0) { g_title.color = (unsigned)strtoul(lead, NULL, 0); } } } else if (section == 10) { char key[64], val[512]; if (sscanf(s, "%63s = %511[^\n\r]", key, val) >= 2) { int vlen = (int)strlen(val); while (vlen > 0 && (val[vlen-1] == ' ' || val[vlen-1] == '\t' || val[vlen-1] == '\r' || val[vlen-1] == '\n')) val[--vlen] = '\0'; char *lead = val; while (*lead == ' ' || *lead == '\t') lead++; if (strcmp(key, "file") == 0) { strncpy(g_flog_path, lead, sizeof(g_flog_path) - 1); } else if (strcmp(key, "rotate_ms") == 0) { g_flog_rotate_ms = atol(lead); } else if (strcmp(key, "max_bytes") == 0) { g_flog_max_bytes = atol(lead); } } } else if (section == 17) { char key[64], val[64]; if (sscanf(s, "%63s = %63s", key, val) >= 2) { if (strcmp(key, "enable") == 0) { g_reboot_enable = atoi(val) != 0; } else if (strcmp(key, "period") == 0) { g_reboot_period = (strcasecmp(val, "weekly") == 0) ? 1 : 0; } else if (strcmp(key, "time") == 0) { int hh = 3, mm = 0; if (sscanf(val, "%d:%d", &hh, &mm) >= 1) { g_reboot_hour = (hh >= 0 && hh <= 23) ? hh : 3; g_reboot_min = (mm >= 0 && mm <= 59) ? mm : 0; } } else if (strcmp(key, "weekday") == 0) { const char *dn[] = {"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"}; g_reboot_wday = -1; for (int i = 0; i < 7; i++) if (strcasecmp(val, dn[i]) == 0) g_reboot_wday = i; } } } else if (section == 11) { /* ; used only by single-channel layouts */ int idx; char *url = strchr(s, ' '); if (!url) url = strchr(s, '\t'); if (!url) continue; *url++ = '\0'; idx = atoi(s); while (*url == ' ' || *url == '\t') url++; size_t len = strlen(url); while (len > 0 && (url[len-1] == '\n' || url[len-1] == '\r')) url[--len] = 0; if (idx >= 0 && idx < MAX_STREAMS && len > 0) { size_t cp = len < sizeof(workers[idx].url_main) - 1 ? len : sizeof(workers[idx].url_main) - 1; memcpy(workers[idx].url_main, url, cp); workers[idx].url_main[cp] = '\0'; } } else if (section == 12) { /* [camera_list] [login_chan] [rtsp_port] [http_port] */ int id, chan = 1, port = 554, http_port = 80; char ip[64], user[64], pass[64]; int nf = sscanf(s, "%d %63s %63s %63s %d %d %d", &id, ip, user, pass, &chan, &port, &http_port); if (nf >= 4 && id >= 0 && id < MAX_STREAMS) { HvCam *c = &g_cams[id]; memset(c, 0, sizeof(*c)); c->id = id; strncpy(c->ip, ip, sizeof(c->ip) - 1); strncpy(c->user, user, sizeof(c->user) - 1); strncpy(c->pass, pass, sizeof(c->pass) - 1); c->login_chan = (nf >= 5 && chan > 0) ? chan : 1; c->port = (nf >= 6 && port > 0) ? port : 554; c->http_port = (nf >= 7 && http_port > 0) ? http_port : 80; if (id + 1 > g_num_cams) g_num_cams = id + 1; hv_build_urls(c, &g_urls[id]); } } else if (section == 13) { /* [isapi_write] * 可选:启动时通过 ISAPI 把指定通道改成给定分辨率/帧率/码率。 */ char stream[16]; int id, w = 0, h = 0, fps = 0, br = 0; int nf = sscanf(s, "%d %15s %d %d %d %d", &id, stream, &w, &h, &fps, &br); if (nf == 6 && id >= 0 && id < MAX_STREAMS && g_isapi_write_count < MAX_ISAPI_WRITE) { IsapiWrite *r = &g_isapi_write[g_isapi_write_count]; r->id = id; r->main = (strcmp(stream, "main") == 0); r->width = w; r->height = h; r->fps = fps; r->bitrate = br; g_isapi_write_count++; } } } fclose(fp); if (g_num_cams > 0) { /* A camera list entirely replaces the manual streams: one worker per * camera, using ISAPI-derived sub/main RTSP URLs. */ nstreams = g_num_cams; for (int i = 0; i < nstreams && i < MAX_STREAMS; i++) { strncpy(workers[i].url, g_urls[i].sub, sizeof(workers[i].url) - 1); strncpy(workers[i].url_main, g_urls[i].main, sizeof(workers[i].url_main) - 1); } } if (nstreams == 0) return -1; if (ntiles == 0) { ntiles = nstreams; for (int i = 0; i < ntiles; i++) tilemap[i] = i; } if (g_reconnect_ms < 100) g_reconnect_ms = 100; if (g_reconnect_max_ms < g_reconnect_ms) g_reconnect_max_ms = g_reconnect_ms; if (g_connect_timeout_sec < 1) g_connect_timeout_sec = 1; if (g_hold_layout_sec < 0) g_hold_layout_sec = 0; if (g_layout_debounce_ms < 0) g_layout_debounce_ms = 0; *p_nstreams = nstreams; *p_ntiles = ntiles; return 0; } /* HikVision ISAPI startup: optional parameter writes first, then read-and-log * the encoding params of every configured camera. Each request can block up * to 8 s, so this runs on a detached background thread and must not delay * video startup. Uses g_cams/g_urls which are only written during * parse_config (before threads start). */ static void *isapi_startup_thread(void *arg) { (void)arg; for (int w = 0; w < g_isapi_write_count; w++) { IsapiWrite *r = &g_isapi_write[w]; if (r->id < 0 || r->id >= g_num_cams) continue; int enc = (g_cams[r->id].login_chan * 100) + (r->main ? 1 : 2); /* Hikvision stores frame rate x100 (e.g. 25fps -> 2500). */ hv_write_params(&g_cams[r->id], enc, r->width, r->height, r->fps * 100, r->bitrate); } for (int i = 0; i < g_num_cams; i++) { if (!g_running) break; hv_read_and_log(&g_cams[i], g_cams[i].login_chan * 100 + 1); hv_read_and_log(&g_cams[i], g_cams[i].login_chan * 100 + 2); } return NULL; } /* Scheduled reboot ([auto_reboot]): wake up exactly at HH:MM (local time), * daily or on the configured weekday, then reboot the device once. The reboot * is executed by a forked child so a hang in the reboot path can't wedge the * running application. */ static void reboot_now(void) { fprintf(stderr, "DBG: auto reboot triggered at %ld\n", (long)time(NULL)); sync(); pid_t pid = fork(); if (pid == 0) { setsid(); sleep(2); execl("/sbin/reboot", "reboot", (char *)NULL); _exit(127); } } static void *reboot_thread(void *arg) { (void)arg; while (1) { time_t now = time(NULL); struct tm t; localtime_r(&now, &t); struct tm cand = t; time_t next = 0; for (int guard = 0; guard < 10; guard++) { struct tm c = cand; c.tm_sec = 0; c.tm_min = g_reboot_min; c.tm_hour = g_reboot_hour; if (g_reboot_period == 1 && g_reboot_wday >= 0) { time_t w = mktime(&c); struct tm m; localtime_r(&w, &m); if (m.tm_wday != g_reboot_wday) { cand.tm_mday++; continue; } } next = mktime(&c); if (next > now) break; cand.tm_mday++; } long dt = (long)(next - now); if (dt < 15 || dt > 2 * 24 * 3600) dt = 24 * 3600; struct timespec ts; ts.tv_sec = dt; ts.tv_nsec = 0; nanosleep(&ts, NULL); if (g_reboot_enable) reboot_now(); } return NULL; } static void display_rebuild_now(void) { fprintf(stderr, "display: rebuilding output\n"); if (g_display) display_destroy(g_display); for (int tries = 0; tries < 10 && g_running; tries++) { g_display = display_create(NULL); if (!g_display) { fprintf(stderr, "display: recreate failed, retrying in 1s\n"); sleep(1); continue; } usleep(500000); /* Ask for the size our existing output buffers were allocated at, so * display_init prefers a matching connector mode. Without this a * monitor hot-plug can re-enumerate a different preferred mode and the * scanout buffer no longer matches the CRTC (drmModeAddFB2 EINVAL * rebuild loop). */ if (display_init(g_display, output_w, output_h) == 0) { fprintf(stderr, "display: rebuilt OK\n"); return; } fprintf(stderr, "display: init deferred (no monitor), retrying in 1s\n"); display_destroy(g_display); g_display = NULL; sleep(1); } fprintf(stderr, "display: rebuild deferred, will retry later\n"); } int main(int argc, char **argv) { fprintf(stderr, "DBG: starting\n"); install_signal_handlers(); const char *config_path = NULL; if (argc >= 3 && strcmp(argv[1], "-c") == 0) { fprintf(stderr, "DBG: parsing config\n"); config_path = argv[2]; if (parse_config(config_path, &output_w, &output_h, &num_streams, &num_tiles, tile_map) < 0) { fprintf(stderr, "Failed to parse config: %s\n", config_path); return 1; } fprintf(stderr, "DBG: parsed: %dx%d %d streams %d tiles\n", output_w, output_h, num_streams, num_tiles); if (flog_init(g_flog_path, g_flog_rotate_ms, g_flog_max_bytes) < 0) fprintf(stderr, "WARN: fault log disabled (%s)\n", g_flog_path); /* ISAPI read/write on a background thread: video must not wait on * camera HTTP round-trips. */ if (g_num_cams > 0) { pthread_t isapi_thr; if (pthread_create(&isapi_thr, NULL, isapi_startup_thread, NULL) == 0) pthread_detach(isapi_thr); } } else if (argc >= 3) { output_w = atoi(argv[1]); output_h = atoi(argv[2]); num_streams = argc - 3; if (num_streams > MAX_STREAMS) num_streams = MAX_STREAMS; if (num_streams < 1) { printf("At least 1 RTSP URL required\n"); return 1; } num_tiles = num_streams; for (int i = 0; i < num_tiles; i++) { tile_map[i] = i; strncpy(workers[i].url, argv[3 + i], sizeof(workers[i].url) - 1); } } else { printf("Usage 1: %s [url2...]\n", argv[0]); printf("Usage 2: %s -c \n", argv[0]); printf("Config sections: [output], [streams], [main_streams], [camera_list],\n"); printf(" [isapi_write], [layout], [layoutN], [plc], [plc_rules], [title], [log]\n"); return 1; } if (g_plc_enabled && g_plc_cfg.ip[0]) { if (g_plc_cfg.port <= 0) g_plc_cfg.port = 102; if (g_plc_cfg.area == 0) g_plc_cfg.area = PLC_AREA_M; if (g_plc_cfg.read_len <= 0) g_plc_cfg.read_len = 1; if (g_plc_cfg.tsap_local == -1) g_plc_cfg.tsap_local = 0x0100; if (g_plc_cfg.tsap_remote == -1) g_plc_cfg.tsap_remote = 0x0102; if (plc_init(&g_plc_cfg) < 0) { fprintf(stderr, "WARN: plc_init failed (%s)\n", plc_last_error()); g_plc_enabled = 0; } else { fprintf(stderr, "DBG: plc enabled %s:%d area=0x%02x db=%d start=%d len=%d\n", g_plc_cfg.ip, g_plc_cfg.port, g_plc_cfg.area, g_plc_cfg.db, g_plc_cfg.read_start, g_plc_cfg.read_len); } } /* Start embedded web config server */ if (config_path) { web_server_start(config_path, WEB_SERVER_PORT_DEFAULT, WEB_SERVER_PASS_DEFAULT); } if (g_reboot_enable) { pthread_t rbt; if (pthread_create(&rbt, NULL, reboot_thread, NULL) == 0) { pthread_detach(rbt); fprintf(stderr, "DBG: auto reboot on, %02d:%02d %s\n", g_reboot_hour, g_reboot_min, g_reboot_period == 1 ? "weekly" : "daily"); } } fprintf(stderr, "DBG: output %dx%d %d streams %d tiles\n", output_w, output_h, num_streams, num_tiles); /* Force the HDMI output to RGB: after a hotplug the VOP's YUV passthrough * path renders with wrong colours (purple wall); RGB keeps the window on * a full Y2R CSC path and the monitor locked to a deterministic format. */ display_set_color_format(1); g_display = display_create(NULL); if (!g_display) { fprintf(stderr, "Failed to create display\n"); return 1; } /* If the board powers up without a monitor, the DRM connector has no mode * and display_init fails ("display: no monitor ... retry"). Retry once per * second so the video appears automatically as soon as a monitor is * plugged in; without this the desktop stays up forever. */ while (g_running && display_init(g_display, 0, 0) < 0) { fprintf(stderr, "display: init deferred (no monitor connected), retrying in 1s\n"); display_destroy(g_display); g_display = display_create(NULL); if (!g_display) { fprintf(stderr, "Failed to create display\n"); return 1; } sleep(1); } if (!g_running) { /* stop requested while waiting for a monitor */ display_destroy(g_display); return 0; } output_w = display_get_width(g_display); output_h = display_get_height(g_display); fprintf(stderr, "DBG: display resolution %dx%d\n", output_w, output_h); /* Title bar: enabled only if configured and height fits the output. * bar = solid bar, reserves g_title_h rows (video area shifted down); * overlay = translucent strip drawn on top of the video, no space taken. */ g_title_h = 0; g_title_overlay = (g_title.enable && g_title.type != 0); if (g_title.enable && g_title.height > 0 && g_title.height < output_h) { if (!g_title_overlay) { g_title_h = g_title.height; if (g_title_h & 1) g_title_h--; /* keep UV rows aligned */ if (g_title_h < 2) g_title_h = 0; } } output_stride = output_w; output_size = output_w * output_h * 3 / 2; for (int i = 0; i < 3; i++) { /* Output buffers feed RGA as dmabuf fds. The RGA driver refuses any * buffer whose physical pages sit above 4G ("RGA_MMU unsupported * memory larger than 4G"). The board maps part of DDR above 4G and * CMA is only 16MB, so prefer the dma32 heap (guaranteed <4G); * cma is the next fallback (also <4G), plain memory last. */ if (dma_heap_alloc("system-uncached-dma32", output_size, &output_fd[i]) < 0) { if (dma_heap_alloc("cma", output_size, &output_fd[i]) < 0) { if (dma_heap_alloc("system-uncached", output_size, &output_fd[i]) < 0) { fprintf(stderr, "Failed to allocate DMA buffer %d\n", i); return 1; } } } output_map[i] = mmap(NULL, output_size, PROT_READ | PROT_WRITE, MAP_SHARED, output_fd[i], 0); if (output_map[i] == MAP_FAILED) { fprintf(stderr, "mmap failed for output buffer %d\n", i); return 1; } memset(output_map[i], 0x80, output_size); memset(output_map[i], 0x10, output_w * output_h); } /* Render the title bar once into all buffers (runtime cost = 0). * In overlay mode this covers only the initial frame; the main loop * re-draws it on top of every composed frame. */ if (g_title_h > 0 || g_title_overlay) { int ok = 1; for (int i = 0; i < 3; i++) { struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[i], DMA_BUF_IOCTL_SYNC, &sync); if (g_title_overlay) { if (title_render_overlay(output_map[i], output_w, output_h, &g_title) != 0) ok = 0; } else { if (title_render(output_map[i], output_w, output_h, &g_title) != 0) ok = 0; } struct dma_buf_sync sync2 = { .flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[i], DMA_BUF_IOCTL_SYNC, &sync2); } if (ok) { g_title_enabled = 1; fprintf(stderr, "DBG: title %s %dx%d enabled\n", g_title_overlay ? "overlay" : "bar", output_w, g_title_overlay ? g_title.height : g_title_h); } else { /* fall back to no title bar: clear bar area and drop offset */ fprintf(stderr, "WARN: title render failed, disabling title bar\n"); g_title_h = 0; g_title_overlay = 0; for (int i = 0; i < 3; i++) { memset(output_map[i], 0x80, output_size); memset(output_map[i], 0x10, output_w * output_h); } } } else { fprintf(stderr, "DBG: title bar disabled\n"); } g_composer = composer_create(output_w, output_h, num_tiles, g_title_h); if (!g_composer) { fprintf(stderr, "Failed to create composer\n"); display_destroy(g_display); return 1; } composer_set_letterbox(g_composer, g_letterbox); /* Layout the base grid. If the config gave row counts (via [layout]), * compile it into merged regions; otherwise use a uniform grid. */ int flow_ok = 0; if (g_split_enabled && g_split_top_n > 0) { /* Split mode: start with the [layout_bottom] default group, fallback * to the highest valid [layout_bottom_N]. */ int bottom_n = split_idle_bottom(); int *bid = NULL; int bottom_cnt = split_bottom_cfg(bottom_n, &bid); if (bottom_cnt < 1) bottom_cnt = 1; int xs[MAX_TILES], ys[MAX_TILES], ws[MAX_TILES], hs[MAX_TILES], sid_arr[MAX_TILES]; int n = composer_split_rects( output_w, output_h, g_title_h, g_split_top_ids, g_split_top_n, g_split_top_ratio, bid, bottom_cnt, bottom_cnt, xs, ys, ws, hs, sid_arr); if (n > 0) { composer_set_tiles(g_composer, xs, ys, ws, hs, n); g_disp_tiles = n; for (int t = 0; t < n; t++) g_disp_sid[t] = (sid_arr[t] >= 0 && sid_arr[t] < MAX_STREAMS) ? sid_arr[t] : -1; flow_ok = 1; fprintf(stderr, "DBG: split mode init top=%d bottom_idle=%d bottom_cnt=%d ratio=%.1f\n", g_split_top_n, bottom_n, bottom_cnt, g_split_top_ratio); } } if (!flow_ok && g_grid_nrows > 0) { int xs[MAX_TILES], ys[MAX_TILES], ws[MAX_TILES], hs[MAX_TILES], idx[MAX_TILES]; int n = composer_region_rects(output_w, output_h, g_grid_row_cols, g_grid_nrows, tile_map, xs, ys, ws, hs, idx, g_title_h); if (n > 0) { composer_set_tiles(g_composer, xs, ys, ws, hs, n); g_disp_tiles = n; for (int t = 0; t < n; t++) g_disp_sid[t] = (idx[t] >= 0 && idx[t] < MAX_STREAMS) ? idx[t] : -1; flow_ok = 1; } } if (!flow_ok) { for (int i = 0; i < num_tiles; i++) { composer_get_tile_layout(g_composer, i, &g_grid_x[i], &g_grid_y[i], &g_grid_w[i], &g_grid_h[i]); } g_disp_tiles = num_tiles; for (int t = 0; t < g_disp_tiles; t++) g_disp_sid[t] = (tile_map[t] >= 0 && tile_map[t] < MAX_STREAMS) ? tile_map[t] : -1; } else { for (int i = 0; i < g_disp_tiles && i < MAX_TILES; i++) { int x, y, w, h; composer_get_tile_layout(g_composer, i, &x, &y, &w, &h); g_grid_x[i] = x; g_grid_y[i] = y; g_grid_w[i] = w; g_grid_h[i] = h; } } fprintf(stderr, "DBG: layouts=%d rules=%d grid%d split=%d\n", g_num_layouts, g_num_rules, flow_ok ? g_grid_nrows : -1, g_split_enabled); output_front = 0; output_last = 1; /* so first BACK_BUF() = 2 (not front, not last) */ if (display_commit(g_display, output_fd[output_front], output_stride) < 0) fprintf(stderr, "Warning: initial display_commit failed\n"); for (int i = 0; i < num_streams; i++) { StreamWorker *w = &workers[i]; w->stream_id = i; w->running = 1; w->connected = 0; w->dec = NULL; w->ring_count = 0; w->ring_head = 0; w->frame_seq = 0; w->held_pin = -1; w->use_main = 0; w->dec_using_main = 0; w->last_fd = -1; w->last_w = 0; w->last_h = 0; w->last_stride = 0; w->last_vstride = 0; w->last_seq_seen = -1; w->last_frame_mono = -1e9; pthread_mutex_init(&w->mutex, NULL); pthread_cond_init(&w->cond, NULL); pthread_create(&w->thread, NULL, worker_thread, w); } int active_streams = 0, connect_wait = 0; while (g_running && active_streams == 0 && connect_wait < 200) { active_streams = 0; for (int i = 0; i < num_streams; i++) if (workers[i].connected) active_streams++; if (active_streams == 0) { usleep(50000); connect_wait++; } } if (active_streams != num_streams) printf("Proceeding with %d/%d connected streams\n", active_streams, num_streams); fprintf(stderr, "DBG: entering main loop\n"); printf("Entering display loop (%d tiles from %d streams)...\n", num_tiles, num_streams); int frame_count = 0; struct timeval last_tv; gettimeofday(&last_tv, NULL); struct timeval comp_stats_last = last_tv; int held_idx[MAX_STREAMS]; int held_valid[MAX_STREAMS]; memset(held_valid, 0, sizeof(held_valid)); static int disp_was_lost = -1; /* hotplug edge state */ static double last_disp_rebuild = -1e9; while (g_running) { /* Hotplug recovery: when an HDMI switch points this display away our * connector reports disconnected; once it points back we must rebuild * the DRM output (Xorg/desktop may have taken the CRTC meanwhile), or * the screen stays on the desktop. Detect the lost->connected edge * and re-create the display. */ if (g_rebuild_req) { g_rebuild_req = 0; fprintf(stderr, "display: SIGUSR1 rebuild requested\n"); display_rebuild_now(); } if (g_display) { int lost = display_signal_lost(g_display); double now_m = 0; struct timespec mt2; clock_gettime(CLOCK_MONOTONIC, &mt2); now_m = mt2.tv_sec + mt2.tv_nsec / 1000000000.0; if (disp_was_lost == 1 && lost == 0 && now_m - last_disp_rebuild > 0.3) { last_disp_rebuild = now_m; fprintf(stderr, "display: monitor reconnected, rebuilding output\n"); display_rebuild_now(); } disp_was_lost = lost ? 1 : 0; } else { /* No live display (rebuild gave up while unplugged): keep retrying * so a later plug-in brings the picture back by itself. */ double now_m = 0; struct timespec mt2; clock_gettime(CLOCK_MONOTONIC, &mt2); now_m = mt2.tv_sec + mt2.tv_nsec / 1000000000.0; if (now_m - last_disp_rebuild > 0.2) { last_disp_rebuild = now_m; display_rebuild_now(); } } int src_fds[MAX_STREAMS]; int src_widths[MAX_STREAMS]; int src_heights[MAX_STREAMS]; int src_strides[MAX_STREAMS]; int src_vstrides[MAX_STREAMS]; int valid_srcs = 0; struct timespec mts; clock_gettime(CLOCK_MONOTONIC, &mts); double mono_now = mts.tv_sec + mts.tv_nsec / 1000000000.0; for (int i = 0; i < num_streams; i++) { StreamWorker *w = &workers[i]; src_fds[i] = -1; held_valid[i] = 0; pthread_mutex_lock(&w->mutex); if (!w->connected) { /* Worker is tearing down / reconnecting: drain stale frames. */ for (int j = 0; j < w->ring_count; j++) { int ri = (w->ring_head + j) % 2; if (w->ring[ri].idx >= 0 && w->dec) decoder_release_frame(w->dec, w->ring[ri].idx); if (w->ring[ri].fd >= 0) close(w->ring[ri].fd); w->ring[ri].fd = -1; w->ring[ri].idx = -1; } w->ring_count = 0; w->held_pin = -1; pthread_cond_broadcast(&w->cond); pthread_mutex_unlock(&w->mutex); continue; } if (w->ring_count > 0) { int latest = (w->ring_head + w->ring_count - 1) % 2; src_fds[i] = fcntl(w->ring[latest].fd, F_DUPFD_CLOEXEC, 3); src_widths[i] = w->ring[latest].width; src_heights[i] = w->ring[latest].height; src_strides[i] = w->ring[latest].hor_stride; src_vstrides[i] = w->ring[latest].ver_stride; held_idx[i] = w->ring[latest].idx; /* A new frame arrived (seq changed): timestamp it. If the * worker is stuck on a silent stream the same seq keeps being * re-acquired, so this stays stale and the tile eventually * falls back to NO SIGNAL. */ if (w->ring[latest].pts != w->last_seq_seen) { w->last_seq_seen = w->ring[latest].pts; w->last_frame_mono = mono_now; /* Refresh the frozen last-frame handle (only on new frames; * the dup stays valid even after the decoder is destroyed * on a reconnect, so the tile can keep showing this frame * during the gap). */ if (w->last_fd >= 0) close(w->last_fd); w->last_fd = fcntl(w->ring[latest].fd, F_DUPFD_CLOEXEC, 3); w->last_w = w->ring[latest].width; w->last_h = w->ring[latest].height; w->last_stride = w->ring[latest].hor_stride; w->last_vstride = w->ring[latest].ver_stride; } /* Release dropped frames (all except latest). */ for (int j = 0; j < w->ring_count - 1; j++) { int ri = (w->ring_head + j) % 2; if (w->dec) decoder_release_frame(w->dec, w->ring[ri].idx); if (w->ring[ri].fd >= 0) close(w->ring[ri].fd); w->ring[ri].fd = -1; } w->ring_count = 1; w->ring_head = latest; w->held_pin = held_idx[i]; held_valid[i] = 1; valid_srcs++; } else { w->held_pin = -1; } pthread_cond_broadcast(&w->cond); pthread_mutex_unlock(&w->mutex); } /* --- PLC-driven layout selection --- */ int active_layout = -1; /* -1 = grid ([layout]) */ int plc_ok = g_plc_enabled && plc_is_connected(); if (plc_ok) { /* Among matching rules, the most specific one wins: * more groups first, then more bits, then earlier in file. */ int best_g = -1, best_b = -1; for (int r = 0; r < g_num_rules; r++) { PlcRule *R = &g_rules[r]; /* Default: any bit inside a ';'-group is enough (OR within a * group) and every group must be satisfied (AND across * groups), e.g. `3 : 6.6,6.7;8.4,8.5` -> (6.6|6.7)&(8.4|8.5). * Trailing `: all` also requires every bit inside a group. */ int match = 1; for (int g = 0; g < R->ngroups; g++) { int gok = R->all ? 1 : 0; for (int b = 0; b < R->nbits; b++) { if (R->group[b] != g) continue; int v = plc_get_bit(R->bits[b].byte_idx, R->bits[b].bit); if (R->all) { if (v <= 0) { gok = 0; break; } } else { if (v < 0) continue; if (v) { gok = 1; break; } } } if (!gok) { match = 0; break; } } if (!match) continue; if (R->ngroups > best_g || (R->ngroups == best_g && R->nbits > best_b)) { best_g = R->ngroups; best_b = R->nbits; active_layout = R->layout; } } } struct timeval layout_tv; gettimeofday(&layout_tv, NULL); double layout_now = layout_tv.tv_sec + layout_tv.tv_usec / 1000000.0; if (plc_ok) { g_last_layout_ts = layout_now; } else if (g_hold_layout_sec > 0 && g_cur_layout >= 0 && layout_now - g_last_layout_ts < g_hold_layout_sec) { /* PLC briefly lost: keep the last good layout. */ active_layout = g_cur_layout; } if (active_layout != g_cur_layout) { /* Debounce: only commit a layout change once the new layout has * been stable for g_layout_debounce_ms. A momentary PLC bit pulse * (point triggers then clears within one scan) is thus ignored and * the screen does not flicker. The very first layout applies at * once so there is no startup delay. */ int do_switch; if (g_cur_layout < 0 || g_layout_debounce_ms <= 0) { do_switch = 1; g_pending_layout = -1; } else { if (active_layout != g_pending_layout) { g_pending_layout = active_layout; g_pending_layout_ts = layout_now; } do_switch = (layout_now - g_pending_layout_ts) * 1000.0 >= g_layout_debounce_ms; } if (do_switch) { g_pending_layout = -1; /* ---- Split mode: fixed top + adaptive bottom ---- */ if (g_split_enabled && g_split_top_n > 0) { int bottom_n; if (active_layout < 0) { /* No PLC rule matched: default bottom ([layout_bottom]). */ bottom_n = split_idle_bottom(); } else { bottom_n = active_layout + 1; /* layout 0→1cam, 1→2cam, 2→3cam */ if (bottom_n < 1) bottom_n = 1; if (bottom_n > SPLIT_MAX_BOTTOM) bottom_n = SPLIT_MAX_BOTTOM; } if (bottom_n > 0 && !g_split_bottom_valid[bottom_n - 1]) { /* Fallback: find first valid bottom variant */ for (int bv = SPLIT_MAX_BOTTOM; bv >= 1; bv--) if (g_split_bottom_valid[bv - 1]) { bottom_n = bv; break; } } int *bid = NULL; int bottom_cnt = split_bottom_cfg(bottom_n, &bid); if (bottom_cnt < 1) bottom_cnt = 1; int xs[MAX_TILES], ys[MAX_TILES], ws[MAX_TILES], hs[MAX_TILES], sid[MAX_TILES]; int placement = composer_split_rects( output_w, output_h, g_title_h, g_split_top_ids, g_split_top_n, g_split_top_ratio, bid, bottom_cnt, bottom_cnt, xs, ys, ws, hs, sid); if (placement > 0) { composer_set_tiles(g_composer, xs, ys, ws, hs, placement); g_disp_tiles = placement; for (int t = 0; t < placement; t++) g_disp_sid[t] = (sid[t] >= 0 && sid[t] < MAX_STREAMS) ? sid[t] : -1; } g_cur_layout = active_layout; } else if (active_layout >= 0 && active_layout < g_num_layouts) { LayoutDef *L = &g_layouts[active_layout]; int ncell = L->n; if (ncell > MAX_TILES) ncell = MAX_TILES; int xs[MAX_TILES], ys[MAX_TILES], ws[MAX_TILES], hs[MAX_TILES]; int idx[MAX_TILES]; /* region -> region sid mapping */ int placement = 0; if (L->nrows > 0) { /* named layout: merged regions for same-label cells * (cells are already flattened in L->map, row-major) */ placement = composer_region_rects( output_w, output_h, L->row_cols, L->nrows, L->map, xs, ys, ws, hs, idx, g_title_h); } if (placement <= 0) { /* fallback: uniform grid */ composer_grid_rects(output_w, output_h, ncell, xs, ys, ws, hs, g_title_h); placement = ncell; for (int t = 0; t < placement; t++) idx[t] = L->map[t]; } composer_set_tiles(g_composer, xs, ys, ws, hs, placement); g_disp_tiles = placement; for (int t = 0; t < placement; t++) g_disp_sid[t] = (idx[t] >= 0 && idx[t] < MAX_STREAMS) ? idx[t] : -1; } else if (g_split_enabled && g_split_top_n > 0) { /* Split mode, no PLC rule: default bottom ([layout_bottom]) */ int bottom_n = split_idle_bottom(); int *bid = NULL; int bottom_cnt = split_bottom_cfg(bottom_n, &bid); if (bottom_cnt < 1) bottom_cnt = 1; int xs[MAX_TILES], ys[MAX_TILES], ws[MAX_TILES], hs[MAX_TILES], sid_arr[MAX_TILES]; int placement = composer_split_rects( output_w, output_h, g_title_h, g_split_top_ids, g_split_top_n, g_split_top_ratio, bid, bottom_cnt, bottom_cnt, xs, ys, ws, hs, sid_arr); if (placement > 0) { composer_set_tiles(g_composer, xs, ys, ws, hs, placement); g_disp_tiles = placement; for (int t = 0; t < placement; t++) g_disp_sid[t] = (sid_arr[t] >= 0 && sid_arr[t] < MAX_STREAMS) ? sid_arr[t] : -1; } } else { /* base grid: compile to regions as well (merges repeated ids) */ if (g_grid_nrows > 0) { int xs[MAX_TILES], ys[MAX_TILES], ws[MAX_TILES], hs[MAX_TILES], idx[MAX_TILES]; int placement = composer_region_rects( output_w, output_h, g_grid_row_cols, g_grid_nrows, tile_map, xs, ys, ws, hs, idx, g_title_h); if (placement > 0) { composer_set_tiles(g_composer, xs, ys, ws, hs, placement); g_disp_tiles = placement; for (int t = 0; t < placement; t++) g_disp_sid[t] = (idx[t] >= 0 && idx[t] < MAX_STREAMS) ? idx[t] : -1; } else { composer_set_tiles(g_composer, g_grid_x, g_grid_y, g_grid_w, g_grid_h, num_tiles); g_disp_tiles = num_tiles; for (int t = 0; t < g_disp_tiles; t++) g_disp_sid[t] = tile_map[t]; } } else { composer_set_tiles(g_composer, g_grid_x, g_grid_y, g_grid_w, g_grid_h, num_tiles); g_disp_tiles = num_tiles; for (int t = 0; t < g_disp_tiles; t++) g_disp_sid[t] = tile_map[t]; } } fprintf(stderr, "DBG: layout switched to %d\n", active_layout); g_cur_layout = active_layout; } /* do_switch */ } else { /* active_layout returned to the current one before the debounce * elapsed: cancel any pending change so a later pulse starts a * fresh, full debounce window. */ g_pending_layout = -1; } /* --- Single-channel active layout: the shown stream's worker switches to its main-channel URL. A layout is single-channel when it compiles to exactly one region. --- */ { int sid = -1; if (g_disp_tiles == 1) { sid = (g_disp_sid[0] >= 0) ? g_disp_sid[0] : -1; } for (int i = 0; i < num_streams; i++) { StreamWorker *w = &workers[i]; int want = (i == sid && w->url_main[0]); pthread_mutex_lock(&w->mutex); w->use_main = want; pthread_mutex_unlock(&w->mutex); } } if (valid_srcs > 0) { int back = BACK_BUF(); /* Clear the video area only; the title bar rows are drawn once * at startup and must not be wiped each frame. */ { struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[back], DMA_BUF_IOCTL_SYNC, &sync); } memset((uint8_t *)output_map[back] + (size_t)output_w * g_title_h, 0x10, (size_t)output_w * (output_h - g_title_h)); memset((uint8_t *)output_map[back] + (size_t)output_w * output_h + (size_t)output_w * (g_title_h / 2), 0x80, (size_t)output_w * (output_h - g_title_h) / 2); { struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[back], DMA_BUF_IOCTL_SYNC, &sync); } /* --- Composition --- */ int tile_fds[MAX_TILES]; int tile_sids[MAX_TILES]; int tile_widths[MAX_TILES]; int tile_heights[MAX_TILES]; int tile_strides[MAX_TILES]; int tile_vstrides[MAX_TILES]; /* Resolve each stream to its display source: a fresh frame when * available, else the held last frame if the stream is only * briefly quiet (switch/reconnect), else nothing (NO SIGNAL). */ int disp_fd[MAX_STREAMS]; int disp_w[MAX_STREAMS]; int disp_h[MAX_STREAMS]; int disp_stride[MAX_STREAMS]; int disp_vstride[MAX_STREAMS]; for (int i = 0; i < num_streams; i++) { StreamWorker *sw = &workers[i]; disp_fd[i] = -1; if (mono_now - sw->last_frame_mono <= HOLD_LAST_FRAME_MS / 1000.0) { if (src_fds[i] >= 0) { disp_fd[i] = src_fds[i]; disp_w[i] = src_widths[i]; disp_h[i] = src_heights[i]; disp_stride[i] = src_strides[i]; disp_vstride[i] = src_vstrides[i]; } else if (sw->last_fd >= 0) { disp_fd[i] = sw->last_fd; disp_w[i] = sw->last_w; disp_h[i] = sw->last_h; disp_stride[i] = sw->last_stride; disp_vstride[i] = sw->last_vstride; } } } /* Composition uses the precompiled regions of the active layout * (g_disp_sid[] / g_disp_tiles), each region mapping to one camera * and one screen rect already set on the composer. */ int cur_tiles = g_disp_tiles; if (cur_tiles > MAX_TILES) cur_tiles = MAX_TILES; for (int t = 0; t < cur_tiles; t++) { int sid = g_disp_sid[t]; if (sid >= 0 && sid < num_streams && disp_fd[sid] >= 0) { tile_sids[t] = sid; tile_fds[t] = disp_fd[sid]; tile_widths[t] = disp_w[sid]; tile_heights[t] = disp_h[sid]; tile_strides[t] = disp_stride[sid]; tile_vstrides[t] = disp_vstride[sid]; } else { tile_sids[t] = -1; tile_fds[t] = -1; } } int ret = composer_compose(g_composer, tile_fds, tile_widths, tile_heights, tile_strides, tile_vstrides, cur_tiles, output_fd[back], output_stride); if (ret != 0) { flog_throttled("compose", 10000, "合成失败 (compose ret=%d)", ret); } /* Commit even when compose fails: the buffer holds the fresh * dark clear, and committing keeps the pipeline alive so the * screen auto-recovers once RGA is healthy again. */ { /* Invalidate the CPU cache so the CPU-side overlays (NO * SIGNAL / detection / translucent title) blend against the * freshly written pixels, not stale ones. Without this, the * translucent title strip tears when the content under it * changes (e.g. layout switch). */ { struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[back], DMA_BUF_IOCTL_SYNC, &sync); } /* --- Draw detection boxes + NO SIGNAL overlays --- */ if (g_grid_lines) draw_grid_lines((uint8_t *)output_map[back], output_w, output_h, g_composer, cur_tiles); if (g_no_signal_text) { for (int t = 0; t < cur_tiles; t++) { if (tile_sids[t] >= 0) continue; int tile_x, tile_y, tile_w, tile_h; composer_get_tile_layout(g_composer, t, &tile_x, &tile_y, &tile_w, &tile_h); int fsz = tile_h / 4; if (fsz < 16) fsz = 16; if (fsz > 72) fsz = 72; title_render_text(output_map[back], output_w, output_h, g_title.font, "NO SIGNAL", fsz, 0xC0C0C0, tile_x + tile_w / 2, tile_y + tile_h / 2); } } if (g_title_overlay && g_title_enabled) { title_render_overlay(output_map[back], output_w, output_h, &g_title); } if (g_plc_enabled) { title_render_plc_status(output_map[back], output_w, output_h, g_title.font, plc_is_connected()); } update_ip_text(); title_render_ip(output_map[back], output_w, output_h, g_title.font, g_ip_text); log_plc_state_change(); __sync_synchronize(); /* Flush CPU-written detection boxes so RGA DMA reads them correctly */ { struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[back], DMA_BUF_IOCTL_SYNC, &sync); } if (g_display && display_commit(g_display, output_fd[back], output_stride) == 0) { output_last = output_front; output_front = back; frame_count++; display_fail_streak = 0; } else { flog_throttled("commit", 10000, "显示提交失败 (display_commit)"); if (g_display && ++display_fail_streak >= 5) { display_fail_streak = 0; display_rebuild_now(); } } } } else { /* All streams dead / never connected: show a dark NO SIGNAL frame * instead of freezing the last frame. Nothing changes between * iterations here, so throttle to ~5 fps instead of burning the * CPU + display pipeline at vblank rate. */ static double last_nosig_commit = -1e9; if (mono_now - last_nosig_commit < 0.2) { log_plc_state_change(); goto release_held; } last_nosig_commit = mono_now; int back = BACK_BUF(); { struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[back], DMA_BUF_IOCTL_SYNC, &sync); } memset((uint8_t *)output_map[back] + (size_t)output_w * g_title_h, 0x08, (size_t)output_w * (output_h - g_title_h)); memset((uint8_t *)output_map[back] + (size_t)output_w * output_h + (size_t)output_w * (g_title_h / 2), 0x80, (size_t)output_w * (output_h - g_title_h) / 2); if (g_grid_lines) draw_grid_lines((uint8_t *)output_map[back], output_w, output_h, g_composer, g_disp_tiles); if (g_no_signal_text) { title_render_text(output_map[back], output_w, output_h, g_title.font, "NO SIGNAL", 96, 0xC0C0C0, 0, output_h / 2); } if (g_title_overlay && g_title_enabled) { title_render_overlay(output_map[back], output_w, output_h, &g_title); } update_ip_text(); title_render_ip(output_map[back], output_w, output_h, g_title.font, g_ip_text); log_plc_state_change(); { struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE }; ioctl(output_fd[back], DMA_BUF_IOCTL_SYNC, &sync); } if (g_display && display_commit(g_display, output_fd[back], output_stride) == 0) { output_last = output_front; output_front = back; frame_count++; display_fail_streak = 0; } else { flog_throttled("commit", 10000, "显示提交失败 (display_commit)"); if (g_display && ++display_fail_streak >= 5) { display_fail_streak = 0; display_rebuild_now(); } } } /* --- Release held decoder frames --- */ release_held: for (int i = 0; i < num_streams; i++) { StreamWorker *w = &workers[i]; if (src_fds[i] >= 0) close(src_fds[i]); pthread_mutex_lock(&w->mutex); if (held_valid[i]) { if (w->dec) decoder_release_frame(w->dec, held_idx[i]); held_valid[i] = 0; w->held_pin = -1; } pthread_cond_broadcast(&w->cond); pthread_mutex_unlock(&w->mutex); } struct timeval now; gettimeofday(&now, NULL); double elapsed = (now.tv_sec - last_tv.tv_sec) + (now.tv_usec - last_tv.tv_usec) / 1000000.0; if (elapsed >= 5.0) { printf("Display FPS: %.1f\n", frame_count / elapsed); frame_count = 0; last_tv = now; } /* Every 15s report per-tile compose health (dirty tiles only) so a * recurring "black flashing tile" is attributable from the log. */ if (now.tv_sec - comp_stats_last.tv_sec >= 15) { composer_dump_stats(g_composer); comp_stats_last = now; } flog_rotate_check(); usleep(1000); } printf("Shutting down...\n"); flog_shutdown(); if (g_plc_enabled) plc_shutdown(); for (int i = 0; i < num_streams; i++) { StreamWorker *w = &workers[i]; w->running = 0; if (w->thread) pthread_join(w->thread, NULL); for (int j = 0; j < 2; j++) { if (w->ring[j].fd >= 0) { close(w->ring[j].fd); w->ring[j].fd = -1; } } if (w->last_fd >= 0) { close(w->last_fd); w->last_fd = -1; } if (w->dec) decoder_destroy(w->dec); pthread_mutex_destroy(&w->mutex); pthread_cond_destroy(&w->cond); } for (int i = 0; i < 3; i++) { if (output_map[i]) munmap(output_map[i], output_size); if (output_fd[i] >= 0) close(output_fd[i]); } composer_destroy(g_composer); display_destroy(g_display); printf("Done.\n"); return 0; }