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:
@@ -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,
|
||||
@@ -638,15 +640,16 @@ struct App_Error proto_read_start_display(
|
||||
struct Proto_Start_Display *out)
|
||||
{
|
||||
if (length < 16) { return APP_INVALID_ERROR_MSG(0, "START_DISPLAY payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->request_id = get_u16(payload, 0);
|
||||
/* skip command word at [2..3] */
|
||||
out->stream_id = get_u16(payload, 4);
|
||||
out->win_x = get_i16(payload, 6);
|
||||
out->win_y = get_i16(payload, 8);
|
||||
out->win_w = get_u16(payload, 10);
|
||||
out->win_h = get_u16(payload, 12);
|
||||
out->scale = get_u8 (payload, 14);
|
||||
out->anchor = get_u8 (payload, 15);
|
||||
out->stream_id = get_u16(payload, 4);
|
||||
out->win_x = get_i16(payload, 6);
|
||||
out->win_y = get_i16(payload, 8);
|
||||
out->win_w = get_u16(payload, 10);
|
||||
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 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -850,10 +941,11 @@ void xorg_viewer_close(Xorg_Viewer *v)
|
||||
if (v->tex_atlas) { glDeleteTextures(1, &v->tex_atlas); }
|
||||
if (v->prog_text) { glDeleteProgram(v->prog_text); }
|
||||
if (v->prog_rect) { glDeleteProgram(v->prog_rect); }
|
||||
if (v->vao) { glDeleteVertexArrays(1, &v->vao); }
|
||||
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->vao) { glDeleteVertexArrays(1, &v->vao); }
|
||||
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 */
|
||||
@@ -1083,9 +1098,10 @@ static void handle_start_display(struct Node *node,
|
||||
d->win_y = (int)req.win_y;
|
||||
d->win_w = req.win_w > 0 ? (int)req.win_w : 1280;
|
||||
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->wanted_state = DISP_OPEN; /* reconciled by display_loop_tick */
|
||||
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);
|
||||
|
||||
proto_write_control_response(conn, req.request_id, PROTO_STATUS_OK, NULL, 0);
|
||||
|
||||
Reference in New Issue
Block a user