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
+34 -7
View File
@@ -1481,11 +1481,11 @@ void graphics_capture_skip(int screen) {
GRAPHICS_CAPTURE_CV[screen].notify_one();
}
bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
int quality, int divide, uint64_t *timestamp,
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
int divide, uint64_t *timestamp,
int *width, int *height) {
out.clear();
out = nullptr;
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return false;
@@ -1558,9 +1558,39 @@ bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
capture_height = height_new;
}
out = std::move(capture_data);
// status
if (timestamp) {
*timestamp = capture_timestamp;
}
if (width) {
*width = capture_width;
}
if (height) {
*height = capture_height;
}
return true;
}
bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
int quality, int divide, uint64_t *timestamp,
int *width, int *height) {
out.clear();
std::shared_ptr<uint8_t[]> pixels;
int capture_width = 0;
int capture_height = 0;
if (!graphics_capture_receive_raw(
screen, pixels, divide, timestamp, &capture_width, &capture_height)) {
return false;
}
// compress
const bool success = jpeg_encoder::encode(
out, capture_data.get(),
out, pixels.get(),
capture_width, capture_height, quality);
if (!success) {
@@ -1568,9 +1598,6 @@ bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
}
// status
if (timestamp) {
*timestamp = capture_timestamp;
}
if (width) {
*width = capture_width;
}
+6
View File
@@ -1,6 +1,7 @@
#pragma once
#include <atomic>
#include <memory>
#include <string>
#include <vector>
#include <optional>
@@ -145,6 +146,11 @@ void graphics_capture_trigger(int screen);
bool graphics_capture_consume(int *screen);
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height);
void graphics_capture_skip(int screen);
// on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
int divide = 0,
uint64_t *timestamp = nullptr,
int *width = nullptr, int *height = nullptr);
// on success `out` holds the encoded JPEG; its storage is reused across calls
bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
int quality = 80, int divide = 0,