Add no-signal animation to display windows
When a viewer window has no incoming stream, renders animated analog-TV noise (hash-based, scanlines, phosphor tint) at configurable fps (default 15) with a centred "NO SIGNAL" text overlay. - xorg: FRAG_NOSIGNAL_SRC shader + xorg_viewer_render_no_signal(v, time, noise_res) - main: Display_Slot gains no_signal_fps + last_no_signal_t; display_loop_tick drives no-signal render on idle slots via clock_gettime rate limiting - protocol: START_DISPLAY extended by 2 bytes — no_signal_fps (0=default 15) + reserved; reader is backward-compatible (defaults 0 if length < 18) - controller_cli: no_signal_fps optional arg on start-display - docs: protocol.md updated with new field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -307,9 +307,9 @@ static void cmd_start_display(struct Transport_Conn *conn,
|
||||
int ntok, char *tokens[])
|
||||
{
|
||||
/* Required: stream_id
|
||||
* Optional: win_x win_y win_w win_h */
|
||||
* Optional: win_x win_y win_w win_h no_signal_fps */
|
||||
if (ntok < 2) {
|
||||
printf("usage: start-display <stream_id> [win_x] [win_y] [win_w] [win_h]\n");
|
||||
printf("usage: start-display <stream_id> [win_x] [win_y] [win_w] [win_h] [no_signal_fps]\n");
|
||||
return;
|
||||
}
|
||||
uint16_t stream_id = (uint16_t)atoi(tokens[1]);
|
||||
@@ -317,12 +317,15 @@ static void cmd_start_display(struct Transport_Conn *conn,
|
||||
int16_t win_y = ntok > 3 ? (int16_t)atoi(tokens[3]) : 0;
|
||||
uint16_t win_w = ntok > 4 ? (uint16_t)atoi(tokens[4]) : 0;
|
||||
uint16_t win_h = ntok > 5 ? (uint16_t)atoi(tokens[5]) : 0;
|
||||
printf("start-display: stream=%u pos=%d,%d size=%ux%u\n",
|
||||
stream_id, win_x, win_y, win_w, win_h);
|
||||
uint8_t no_signal_fps = ntok > 6 ? (uint8_t)atoi(tokens[6]) : 0;
|
||||
printf("start-display: stream=%u pos=%d,%d size=%ux%u no_signal_fps=%u\n",
|
||||
stream_id, win_x, win_y, win_w, win_h,
|
||||
no_signal_fps > 0 ? no_signal_fps : 15);
|
||||
SEND_AND_WAIT(cs, PROTO_CMD_START_DISPLAY,
|
||||
proto_write_start_display(conn, next_req_id(req),
|
||||
stream_id, win_x, win_y, win_w, win_h,
|
||||
PROTO_DISPLAY_SCALE_FIT, PROTO_DISPLAY_ANCHOR_CENTER));
|
||||
PROTO_DISPLAY_SCALE_FIT, PROTO_DISPLAY_ANCHOR_CENTER,
|
||||
no_signal_fps));
|
||||
}
|
||||
|
||||
static void cmd_stop_display(struct Transport_Conn *conn,
|
||||
@@ -345,7 +348,7 @@ static void cmd_help(void)
|
||||
" start-ingest <stream_id> <device> <dest_host> <dest_port>"
|
||||
" [format] [width] [height] [fps_n] [fps_d]\n"
|
||||
" stop-ingest <stream_id>\n"
|
||||
" start-display <stream_id> [win_x] [win_y] [win_w] [win_h]\n"
|
||||
" start-display <stream_id> [win_x] [win_y] [win_w] [win_h] [no_signal_fps]\n"
|
||||
" stop-display <stream_id>\n"
|
||||
" help\n"
|
||||
" quit / exit\n");
|
||||
|
||||
@@ -491,6 +491,8 @@ packet-beta
|
||||
96-111: "win_h"
|
||||
112-119: "scale"
|
||||
120-127: "anchor"
|
||||
128-135: "no_signal_fps"
|
||||
136-143: "reserved"
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
@@ -500,6 +502,7 @@ packet-beta
|
||||
| `win_w`, `win_h` | Window size in pixels; `0` = default (1280×720) |
|
||||
| `scale` | `0`=stretch `1`=fit `2`=fill `3`=1:1 |
|
||||
| `anchor` | `0`=center `1`=topleft |
|
||||
| `no_signal_fps` | Frame rate of no-signal animation (0 = default 15 fps) |
|
||||
|
||||
**Response** — no extra fields beyond request_id and status. `OK` means the display slot was reserved; the window opens asynchronously on the main thread.
|
||||
|
||||
|
||||
@@ -251,6 +251,8 @@ struct Proto_Start_Display {
|
||||
uint16_t win_h;
|
||||
uint8_t scale;
|
||||
uint8_t anchor;
|
||||
uint8_t no_signal_fps; /* 0 = default (15); no-signal animation frame rate */
|
||||
/* 1 byte reserved */
|
||||
};
|
||||
|
||||
struct Proto_Stop_Display {
|
||||
@@ -340,7 +342,7 @@ struct App_Error proto_write_stop_ingest(struct Transport_Conn *conn,
|
||||
struct App_Error proto_write_start_display(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t stream_id,
|
||||
int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h,
|
||||
uint8_t scale, uint8_t anchor);
|
||||
uint8_t scale, uint8_t anchor, uint8_t no_signal_fps);
|
||||
|
||||
/* CONTROL_REQUEST: STOP_DISPLAY */
|
||||
struct App_Error proto_write_stop_display(struct Transport_Conn *conn,
|
||||
|
||||
@@ -67,6 +67,14 @@ void xorg_viewer_set_overlay_text(Xorg_Viewer *v, int idx, int x, int y,
|
||||
/* Remove all text overlays. */
|
||||
void xorg_viewer_clear_overlays(Xorg_Viewer *v);
|
||||
|
||||
/*
|
||||
* Render one frame of animated analog-TV noise with a centred "NO SIGNAL"
|
||||
* label. time is seconds (e.g. glfwGetTime()); noise_res is cells per axis
|
||||
* (lower = coarser, default 80 when 0 is passed).
|
||||
* Call at low frame rate (~15 fps) when the viewer has no incoming stream.
|
||||
*/
|
||||
void xorg_viewer_render_no_signal(Xorg_Viewer *v, float time, float noise_res);
|
||||
|
||||
/*
|
||||
* Process pending window events.
|
||||
* Returns false when the user has closed the window.
|
||||
|
||||
@@ -602,13 +602,13 @@ struct App_Error proto_read_stop_ingest(
|
||||
}
|
||||
|
||||
/* START_DISPLAY: request_id(2) cmd(2) stream_id(2) win_x(2) win_y(2)
|
||||
* win_w(2) win_h(2) scale(1) anchor(1) = 16 bytes */
|
||||
* win_w(2) win_h(2) scale(1) anchor(1) no_signal_fps(1) reserved(1) = 18 bytes */
|
||||
struct App_Error proto_write_start_display(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t stream_id,
|
||||
int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h,
|
||||
uint8_t scale, uint8_t anchor)
|
||||
uint8_t scale, uint8_t anchor, uint8_t no_signal_fps)
|
||||
{
|
||||
uint8_t buf[16];
|
||||
uint8_t buf[18];
|
||||
uint32_t o = 0;
|
||||
put_u16(buf, o, request_id); o += 2;
|
||||
put_u16(buf, o, PROTO_CMD_START_DISPLAY); o += 2;
|
||||
@@ -619,8 +619,10 @@ struct App_Error proto_write_start_display(struct Transport_Conn *conn,
|
||||
put_u16(buf, o, win_h); o += 2;
|
||||
put_u8 (buf, o, scale); o += 1;
|
||||
put_u8 (buf, o, anchor); o += 1;
|
||||
put_u8 (buf, o, no_signal_fps); o += 1;
|
||||
put_u8 (buf, o, 0); o += 1; /* reserved */
|
||||
(void)o;
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 16);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 18);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_stop_display(struct Transport_Conn *conn,
|
||||
@@ -647,6 +649,7 @@ struct App_Error proto_read_start_display(
|
||||
out->win_h = get_u16(payload, 12);
|
||||
out->scale = get_u8 (payload, 14);
|
||||
out->anchor = get_u8 (payload, 15);
|
||||
out->no_signal_fps = length >= 18 ? get_u8(payload, 16) : 0;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,31 @@ static const char *FRAG_YUV_SRC =
|
||||
" out_color = vec4(r, g, b, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
/*
|
||||
* Animated analog-TV noise — no textures, driven by u_time.
|
||||
* u_noise_res: cells per axis (lower = coarser pixels). Default ~80.
|
||||
* Uses the same VERT_SRC quad; u_uv_scale / u_uv_offset set to identity.
|
||||
*/
|
||||
static const char *FRAG_NOSIGNAL_SRC =
|
||||
"#version 330 core\n"
|
||||
"in vec2 v_uv;\n"
|
||||
"out vec4 out_color;\n"
|
||||
"uniform float u_time;\n"
|
||||
"uniform float u_noise_res;\n"
|
||||
"float hash(vec2 p) {\n"
|
||||
" p = fract(p * vec2(127.1, 311.7));\n"
|
||||
" p += dot(p, p + 19.19);\n"
|
||||
" return fract(p.x * p.y);\n"
|
||||
"}\n"
|
||||
"void main() {\n"
|
||||
" vec2 cell = floor(v_uv * u_noise_res) / u_noise_res;\n"
|
||||
" float tick = floor(u_time * 30.0);\n"
|
||||
" float n = hash(cell + tick * 0.017);\n"
|
||||
" float scan = 0.78 + 0.22 * sin(v_uv.y * u_noise_res * 6.2832 * 3.0);\n"
|
||||
" float luma = n * scan;\n"
|
||||
" out_color = vec4(luma * 0.72, luma * 0.80, luma * 0.62, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
/* Passthrough for BGRA — uploaded as GL_BGRA so driver swizzles to RGBA. */
|
||||
static const char *FRAG_RGB_SRC =
|
||||
"#version 330 core\n"
|
||||
@@ -189,6 +214,13 @@ struct Xorg_Viewer {
|
||||
Xorg_Anchor anchor;
|
||||
int frame_w, frame_h;
|
||||
|
||||
/* No-signal noise */
|
||||
GLuint prog_nosignal;
|
||||
GLint u_nosignal_uv_scale;
|
||||
GLint u_nosignal_uv_offset;
|
||||
GLint u_nosignal_time;
|
||||
GLint u_nosignal_res;
|
||||
|
||||
/* Solid rect (overlay background) */
|
||||
GLuint prog_rect;
|
||||
GLint u_rect_loc;
|
||||
@@ -397,6 +429,13 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||
v->u_uv_scale_rgb = glGetUniformLocation(v->prog_rgb, "u_uv_scale");
|
||||
v->u_uv_offset_rgb= glGetUniformLocation(v->prog_rgb, "u_uv_offset");
|
||||
|
||||
v->prog_nosignal = link_program(VERT_SRC, FRAG_NOSIGNAL_SRC);
|
||||
if (!v->prog_nosignal) { xorg_viewer_close(v); return NULL; }
|
||||
v->u_nosignal_uv_scale = glGetUniformLocation(v->prog_nosignal, "u_uv_scale");
|
||||
v->u_nosignal_uv_offset = glGetUniformLocation(v->prog_nosignal, "u_uv_offset");
|
||||
v->u_nosignal_time = glGetUniformLocation(v->prog_nosignal, "u_time");
|
||||
v->u_nosignal_res = glGetUniformLocation(v->prog_nosignal, "u_noise_res");
|
||||
|
||||
glGenVertexArrays(1, &v->vao);
|
||||
glGenTextures(4, v->tex);
|
||||
|
||||
@@ -817,6 +856,58 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* No-signal screen */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/*
|
||||
* Render one frame of animated analog-TV noise with a centred "NO SIGNAL"
|
||||
* label. time is seconds (e.g. from glfwGetTime()); noise_res is cells
|
||||
* per axis — lower = coarser pixels (default: 80).
|
||||
* Call at ~15 fps when the viewer has no live stream to display.
|
||||
*/
|
||||
void xorg_viewer_render_no_signal(Xorg_Viewer *v, float time, float noise_res)
|
||||
{
|
||||
if (!v) { return; }
|
||||
glfwMakeContextCurrent(v->window);
|
||||
|
||||
int fb_w, fb_h;
|
||||
glfwGetFramebufferSize(v->window, &fb_w, &fb_h);
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glViewport(0, 0, fb_w, fb_h);
|
||||
|
||||
glUseProgram(v->prog_nosignal);
|
||||
glUniform2f(v->u_nosignal_uv_scale, 1.0f, 1.0f);
|
||||
glUniform2f(v->u_nosignal_uv_offset, 0.0f, 0.0f);
|
||||
glUniform1f(v->u_nosignal_time, time);
|
||||
glUniform1f(v->u_nosignal_res, noise_res > 0.0f ? noise_res : 80.0f);
|
||||
|
||||
glBindVertexArray(v->vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
/* Measure "NO SIGNAL" text width to centre it. */
|
||||
const char *label = "NO SIGNAL";
|
||||
int text_w = 0, max_ascent = 0;
|
||||
for (const char *p = label; *p; p++) {
|
||||
unsigned char cp = (unsigned char)*p;
|
||||
text_w += font_glyphs[cp].advance;
|
||||
if (font_glyphs[cp].bearing_y > max_ascent) {
|
||||
max_ascent = font_glyphs[cp].bearing_y;
|
||||
}
|
||||
}
|
||||
int tx = (fb_w - text_w) / 2;
|
||||
int ty = (fb_h - max_ascent) / 2;
|
||||
|
||||
xorg_viewer_set_overlay_text(v, 0, tx, ty, label, 1.0f, 1.0f, 1.0f);
|
||||
draw_text_overlays(v, fb_w, fb_h);
|
||||
xorg_viewer_clear_overlays(v);
|
||||
|
||||
glfwSwapBuffers(v->window);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Poll and close */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -854,6 +945,7 @@ void xorg_viewer_close(Xorg_Viewer *v)
|
||||
if (v->tex[0]) { glDeleteTextures(4, v->tex); }
|
||||
if (v->prog_yuv) { glDeleteProgram(v->prog_yuv); }
|
||||
if (v->prog_rgb) { glDeleteProgram(v->prog_rgb); }
|
||||
if (v->prog_nosignal) { glDeleteProgram(v->prog_nosignal); }
|
||||
if (v->window) {
|
||||
glfwDestroyWindow(v->window);
|
||||
glfw_release();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <pthread.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <time.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
@@ -183,6 +184,10 @@ struct Display_Slot {
|
||||
uint32_t frame_len;
|
||||
int frame_ready;
|
||||
|
||||
/* No-signal animation */
|
||||
int no_signal_fps; /* 0 → default 15 */
|
||||
double last_no_signal_t;
|
||||
|
||||
/* Viewer — created and used only on the main thread */
|
||||
Xorg_Viewer *viewer;
|
||||
};
|
||||
@@ -521,6 +526,16 @@ static void display_loop_tick(struct Node *node)
|
||||
xorg_viewer_push_mjpeg(d->viewer, vf.data, vf.data_len);
|
||||
}
|
||||
free(fdata);
|
||||
} else {
|
||||
/* No live frame — render no-signal animation at configured fps */
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
double now = (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
|
||||
double interval = 1.0 / (double)(d->no_signal_fps > 0 ? d->no_signal_fps : 15);
|
||||
if (now - d->last_no_signal_t >= interval) {
|
||||
xorg_viewer_render_no_signal(d->viewer, (float)now, 80.0f);
|
||||
d->last_no_signal_t = now;
|
||||
}
|
||||
}
|
||||
|
||||
/* Poll GLFW events; if user closes the window, treat as STOP_DISPLAY */
|
||||
@@ -1085,6 +1100,7 @@ static void handle_start_display(struct Node *node,
|
||||
d->win_h = req.win_h > 0 ? (int)req.win_h : 720;
|
||||
d->scale = proto_scale_to_xorg(req.scale);
|
||||
d->anchor = proto_anchor_to_xorg(req.anchor);
|
||||
d->no_signal_fps = req.no_signal_fps > 0 ? (int)req.no_signal_fps : 15;
|
||||
d->wanted_state = DISP_OPEN; /* reconciled by display_loop_tick */
|
||||
pthread_mutex_unlock(&d->mutex);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user