| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903 |
- #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;
- }
|