api: h.264 video stream (#876)

## Link to GitHub Issue or related Pull Request, if one exists
fixes #875

## Description of change
Adds `-apistream`, an optional HTTP video stream of the mirrored screen.
It listens on the API port +2.

Two endpoints, sharing the same `screen`, `fps` and `q` parameters:

    /stream.mjpg    JPEG frames, for clients with no container support
/stream.h264 H.264 annex-b, for an app driving MediaCodec or
VideoToolbox itself

One encoder per connection, fed by a per-screen pump that always hands
over the newest frame, so a slow reader drops frames instead of building
a backlog. `capture.get_jpg` behaviour is unchanged.

Additional documentation for developers:
https://github.com/spice2x/spice2x.github.io/wiki/Video-Stream

## Testing
This commit is contained in:
bicarus
2026-08-19 03:10:02 -07:00
committed by GitHub
parent 7c50fcc79e
commit 0934cce225
18 changed files with 1089 additions and 17 deletions
+21
View File
@@ -14,6 +14,7 @@
#include "acio/icca/icca.h"
#include "acio/mdxf/mdxf.h"
#include "api/controller.h"
#include "api/stream_server.h"
#include "avs/automap.h"
#include "avs/core.h"
#include "avs/ea3.h"
@@ -150,6 +151,7 @@ std::string CARD_OVERRIDES[2];
// sub-systems
std::unique_ptr<api::Controller> API_CONTROLLER;
std::unique_ptr<api::StreamServer> API_STREAM_SERVER;
std::unique_ptr<rawinput::RawInputManager> RI_MGR;
// trigger NVIDIA Optimus & AMD Enduro High Performance Graphics
@@ -198,6 +200,7 @@ int main_implementation(int argc, char *argv[]) {
bool api_pretty = false;
bool api_debug = false;
unsigned short api_port = 1337;
bool api_stream_enable = false;
std::string api_pass = "";
std::vector<std::string> api_serial_port;
std::vector<DWORD> api_serial_baud;
@@ -1032,6 +1035,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::APIScreenMirrorDivide].is_active()) {
api::modules::CAPTURE_DIVIDE = options[launcher::Options::APIScreenMirrorDivide].value_uint32();
}
if (options[launcher::Options::APIStreamEnable].value_bool() && !cfg::CONFIGURATOR_STANDALONE) {
api_stream_enable = true;
}
if (options[launcher::Options::DisableDebugHooks].value_bool()) {
debughook::DEBUGHOOK_LOGGING = false;
@@ -2731,6 +2737,20 @@ int main_implementation(int argc, char *argv[]) {
for (size_t i = 0; i < std::min(api_serial_port.size(), api_serial_baud.size()); i++) {
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
}
// the websocket already sits on the API port plus one, so the stream takes plus two
if (api_stream_enable) {
if (!api_enable) {
log_fatal("launcher", "video stream requires API port to be set (-api)");
} else if (api_port + 2 > 65535) {
log_fatal(
"launcher",
"ignoring the video stream, API port {} leaves no room for port plus two",
api_port);
} else {
API_STREAM_SERVER = std::make_unique<api::StreamServer>(
static_cast<unsigned short>(api_port + 2));
}
}
// pin macro
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
@@ -2819,6 +2839,7 @@ int main_implementation(int argc, char *argv[]) {
}
// free api controller
API_STREAM_SERVER.reset();
API_CONTROLLER.reset();
eamuse_pin_macro_stop_thread();