feat(render): fullscreen headless receiver with letterboxing
A headless receiver has no window manager, so a windowed window is meaningless there — the screencast should simply fill the screen. The renderer now goes borderless fullscreen automatically when SDL runs the KMSDRM backend, and --fullscreen forces the same behavior in desktop sessions. The video keeps its aspect ratio via SDL_SetRenderLogicalPresentation(LETTERBOX): a 4:3 desktop on a 16:9 TV renders with black bars instead of a stretched picture, the cursor is hidden, and the clear-before-draw keeps the bars black. Verified live on a 3440x1440 monitor: the --fullscreen receiver window covered the entire display while a 4:3 synthetic feed rendered with pure-black pillarbox bars (Y=0/SAT=0) and a saturated-red center (V=254). meson test 5/5 in both build configurations.
This commit is contained in:
+11
-3
@@ -34,9 +34,17 @@ loopback; current phase is Phase 7.
|
|||||||
- **systemd autostart**: `systemd/screencast-receiver.service` is a
|
- **systemd autostart**: `systemd/screencast-receiver.service` is a
|
||||||
template (`__SC_RECEIVER_BIN__`/`__SC_RECEIVER_USER__`); the install
|
template (`__SC_RECEIVER_BIN__`/`__SC_RECEIVER_USER__`); the install
|
||||||
script substitutes and enables it (opt out: `SC_RECEIVER_SERVICE=0`),
|
script substitutes and enables it (opt out: `SC_RECEIVER_SERVICE=0`),
|
||||||
adding the run user to video/render/input for headless KMSDRM.
|
adding the run user to video/render/input for headless KMSDRM. The unit
|
||||||
Lifecycle-validated with a user unit on this machine: SIGTERM stop →
|
gets a controlling tty (StandardInput=tty, tty1) because SDL's KMSDRM
|
||||||
clean exit (success), mDNS withdrawn, no restart.
|
backend needs one for VT handling.
|
||||||
|
- **Fullscreen headless rendering**: under the KMSDRM video driver (no
|
||||||
|
window manager) the renderer goes fullscreen automatically; --fullscreen
|
||||||
|
forces it on desktops. Aspect is preserved via
|
||||||
|
SDL_SetRenderLogicalPresentation(LETTERBOX) (verified: 4:3 feed on a
|
||||||
|
3440x1440 monitor rendered with black pillarbox bars; --fullscreen
|
||||||
|
window covered the full monitor). The service lifecycle was validated
|
||||||
|
with a user unit: SIGTERM stop → clean exit (success), mDNS withdrawn,
|
||||||
|
no restart.
|
||||||
- **mDNS operational lesson (hit during validation)**: `kill -9` on a
|
- **mDNS operational lesson (hit during validation)**: `kill -9` on a
|
||||||
process holding an avahi registration leaves stale daemon records; the
|
process holding an avahi registration leaves stale daemon records; the
|
||||||
service then browses but never resolves (timeout for every peer) until
|
service then browses but never resolves (timeout for every peer) until
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ installs and enables a systemd service so the receiver starts at boot —
|
|||||||
|
|
||||||
Performance note: decoding is software H.264; on very small boards expect
|
Performance note: decoding is software H.264; on very small boards expect
|
||||||
smooth playback for modest resolutions and reduced frame rates at high
|
smooth playback for modest resolutions and reduced frame rates at high
|
||||||
resolutions. Hardware decode is planned for Phase 7.
|
resolutions. Hardware decode is planned for Phase 7. On a headless console
|
||||||
|
the receiver runs fullscreen automatically with aspect-preserving
|
||||||
|
letterboxing.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ the recommended setup for small boards (Raspberry Pi OS **Lite**):
|
|||||||
3. Disable console blanking: append `consoleblank=0` to
|
3. Disable console blanking: append `consoleblank=0` to
|
||||||
`/boot/firmware/cmdline.txt` and reboot.
|
`/boot/firmware/cmdline.txt` and reboot.
|
||||||
|
|
||||||
|
Under KMSDRM the receiver goes **fullscreen automatically**, with
|
||||||
|
aspect-preserving letterboxing (a 4:3 desktop on a 16:9 TV shows black
|
||||||
|
bars, not a stretched picture) and no mouse cursor. On desktop sessions
|
||||||
|
`--fullscreen` forces the same behavior.
|
||||||
|
|
||||||
A window manager only comes with the desktop-session alternative, where
|
A window manager only comes with the desktop-session alternative, where
|
||||||
the receiver runs inside it (uncomment the `Environment=` lines in the
|
the receiver runs inside it (uncomment the `Environment=` lines in the
|
||||||
service file as described above).
|
service file as described above).
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct ReceiveCommand {
|
|||||||
std::string_view peer_address; // optional, informational
|
std::string_view peer_address; // optional, informational
|
||||||
int local_rtp_port = 5004;
|
int local_rtp_port = 5004;
|
||||||
int signaling_port = 5005;
|
int signaling_port = 5005;
|
||||||
|
bool fullscreen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DiscoverCommand {
|
struct DiscoverCommand {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ struct RendererConfig {
|
|||||||
std::string window_title = "screencast receiver";
|
std::string window_title = "screencast receiver";
|
||||||
int initial_width = 1280;
|
int initial_width = 1280;
|
||||||
int initial_height = 720;
|
int initial_height = 720;
|
||||||
|
// Borderless fullscreen. On the headless KMSDRM backend (no window
|
||||||
|
// manager) fullscreen is enabled automatically.
|
||||||
|
bool fullscreen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
|
|||||||
+3
-1
@@ -9,7 +9,7 @@ namespace {
|
|||||||
|
|
||||||
void print_usage() {
|
void print_usage() {
|
||||||
std::fputs("usage: screencast --send [--target monitor|window] [--peer HOST[:PORT]] [--bitrate KBPS]\n"
|
std::fputs("usage: screencast --send [--target monitor|window] [--peer HOST[:PORT]] [--bitrate KBPS]\n"
|
||||||
" screencast --receive [--port PORT] [--signaling-port PORT]\n"
|
" screencast --receive [--port PORT] [--signaling-port PORT] [--fullscreen]\n"
|
||||||
" screencast --discover [--timeout SECONDS]\n"
|
" screencast --discover [--timeout SECONDS]\n"
|
||||||
"\n"
|
"\n"
|
||||||
"--send without --peer discovers a receiver on the LAN and requires\n"
|
"--send without --peer discovers a receiver on the LAN and requires\n"
|
||||||
@@ -110,6 +110,8 @@ std::optional<Command> parse_cli(int argc, const char* const argv[]) {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
receive.signaling_port = port;
|
receive.signaling_port = port;
|
||||||
|
} else if (argument == "--fullscreen") {
|
||||||
|
receive.fullscreen = true;
|
||||||
} else if (argument == "--timeout") {
|
} else if (argument == "--timeout") {
|
||||||
std::string_view value;
|
std::string_view value;
|
||||||
if (!next_argument(argc, argv, index, value) || !parse_int(value, discover.timeout_seconds) ||
|
if (!next_argument(argc, argv, index, value) || !parse_int(value, discover.timeout_seconds) ||
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ int run_receiver(const sc::ReceiveCommand& command) {
|
|||||||
sc::ReceiverPipelineConfig config;
|
sc::ReceiverPipelineConfig config;
|
||||||
config.local_rtp_endpoint = sc::Endpoint{"0.0.0.0", static_cast<std::uint16_t>(command.local_rtp_port)};
|
config.local_rtp_endpoint = sc::Endpoint{"0.0.0.0", static_cast<std::uint16_t>(command.local_rtp_port)};
|
||||||
config.signaling_port = static_cast<std::uint16_t>(command.signaling_port);
|
config.signaling_port = static_cast<std::uint16_t>(command.signaling_port);
|
||||||
|
// Fullscreen is automatic under KMSDRM (headless); the flag forces it
|
||||||
|
// on desktop sessions.
|
||||||
|
config.renderer.fullscreen = command.fullscreen;
|
||||||
|
|
||||||
sc::ReceiverPipeline pipeline{std::move(config)};
|
sc::ReceiverPipeline pipeline{std::move(config)};
|
||||||
if (!pipeline.start()) {
|
if (!pipeline.start()) {
|
||||||
|
|||||||
@@ -34,12 +34,23 @@ class SdlRenderer final : public Renderer {
|
|||||||
}
|
}
|
||||||
sdl_inited_ = true;
|
sdl_inited_ = true;
|
||||||
|
|
||||||
window_ = SDL_CreateWindow(config_.window_title.c_str(), config_.initial_width, config_.initial_height, 0);
|
// Without a window manager (SDL KMSDRM backend) a windowed window is
|
||||||
|
// meaningless: go fullscreen automatically. --fullscreen forces it
|
||||||
|
// on desktop sessions too.
|
||||||
|
const char* video_driver = SDL_GetCurrentVideoDriver();
|
||||||
|
fullscreen_ = config_.fullscreen || (video_driver != nullptr && std::string_view{video_driver} == "kmsdrm");
|
||||||
|
|
||||||
|
const SDL_WindowFlags window_flags = fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0;
|
||||||
|
window_ =
|
||||||
|
SDL_CreateWindow(config_.window_title.c_str(), config_.initial_width, config_.initial_height, window_flags);
|
||||||
if (window_ == nullptr) {
|
if (window_ == nullptr) {
|
||||||
last_error_ = std::string{"SDL_CreateWindow failed: "} + SDL_GetError();
|
last_error_ = std::string{"SDL_CreateWindow failed: "} + SDL_GetError();
|
||||||
shutdown();
|
shutdown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (fullscreen_) {
|
||||||
|
(void)SDL_HideCursor();
|
||||||
|
}
|
||||||
|
|
||||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||||
if (renderer_ == nullptr) {
|
if (renderer_ == nullptr) {
|
||||||
@@ -76,6 +87,10 @@ class SdlRenderer final : public Renderer {
|
|||||||
if (!SDL_UpdateTexture(texture_, nullptr, frame.rgba_pixels.data(), pitch)) {
|
if (!SDL_UpdateTexture(texture_, nullptr, frame.rgba_pixels.data(), pitch)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Clear first so the letterbox bars stay black between frames.
|
||||||
|
if (!SDL_RenderClear(renderer_)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!SDL_RenderTexture(renderer_, texture_, nullptr, nullptr)) {
|
if (!SDL_RenderTexture(renderer_, texture_, nullptr, nullptr)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -132,6 +147,10 @@ class SdlRenderer final : public Renderer {
|
|||||||
}
|
}
|
||||||
texture_width_ = width;
|
texture_width_ = width;
|
||||||
texture_height_ = height;
|
texture_height_ = height;
|
||||||
|
|
||||||
|
// Present the video at its own aspect ratio: letterbox anything
|
||||||
|
// else (e.g. a 3:2 desktop on a 16:9 TV) instead of stretching.
|
||||||
|
(void)SDL_SetRenderLogicalPresentation(renderer_, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +158,7 @@ class SdlRenderer final : public Renderer {
|
|||||||
std::string last_error_;
|
std::string last_error_;
|
||||||
bool sdl_inited_ = false;
|
bool sdl_inited_ = false;
|
||||||
bool closed_ = false;
|
bool closed_ = false;
|
||||||
|
bool fullscreen_ = false;
|
||||||
SDL_Window* window_ = nullptr;
|
SDL_Window* window_ = nullptr;
|
||||||
SDL_Renderer* renderer_ = nullptr;
|
SDL_Renderer* renderer_ = nullptr;
|
||||||
SDL_Texture* texture_ = nullptr;
|
SDL_Texture* texture_ = nullptr;
|
||||||
|
|||||||
Reference in New Issue
Block a user