|
|
@@ -0,0 +1,1903 @@
|
|
|
+#define _GNU_SOURCE
|
|
|
+#include <stdio.h>
|
|
|
+#include <stdlib.h>
|
|
|
+#include <string.h>
|
|
|
+#include <unistd.h>
|
|
|
+#include <strings.h>
|
|
|
+#include <signal.h>
|
|
|
+#include <pthread.h>
|
|
|
+#include <sys/time.h>
|
|
|
+#include <time.h>
|
|
|
+#include <sys/wait.h>
|
|
|
+#include <errno.h>
|
|
|
+#include <fcntl.h>
|
|
|
+#include <unistd.h>
|
|
|
+#include <sys/ioctl.h>
|
|
|
+#include <linux/dma-heap.h>
|
|
|
+#include <linux/dma-buf.h>
|
|
|
+#include <sys/mman.h>
|
|
|
+#include <ifaddrs.h>
|
|
|
+#include <arpa/inet.h>
|
|
|
+#include <netinet/in.h>
|
|
|
+
|
|
|
+#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) {
|
|
|
+ /* <layout_id> : <byte>.<bit>[,<byte>.<bit>...] [';' ...] [: 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) {
|
|
|
+ /* <index> <main-stream url> ; 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] <id> <ip> <user> <pass> [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] <id> <main|sub> <width> <height> <fps> <bitrate_kbps>
|
|
|
+ * 可选:启动时通过 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 <width> <height> <url1> [url2...]\n", argv[0]);
|
|
|
+ printf("Usage 2: %s -c <config.conf>\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;
|
|
|
+}
|