diff --git a/docs/configuration.md b/docs/configuration.md
index 2d81d8c0dd0..3b9eac0fa4f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -2252,6 +2252,103 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
+### mpeg2_mode
+
+
+
+
Description
+
+ Allows any client implementing the Sunshine codec extension to request MPEG-2/H.262 video streams.
+ Support is unadvertised by default and must be enabled here. Sunshine probes MPEG-2 independently of
+ H.264, preferring the selected backend before trying other available backends. Enabling H.263+ does not
+ displace a working hardware MPEG-2 encoder. Streams using different encoder backends use separate
+ capture workers.
+ @warning{MPEG-2 may be more CPU-intensive to encode than modern hardware-accelerated codecs when hardware
+ encoding is unavailable.}
+
+
+
+
Default
+
@code{}
+ 0
+ @endcode
+
+
+
Example
+
@code{}
+ mpeg2_mode = 2
+ @endcode
+
+
+
Choices
+
0
+
preserve modern codec automatic selection and keep MPEG-2 unadvertised
+
+
+
1
+
do not advertise support for MPEG-2
+
+
+
2
+
advertise support for MPEG-2/H.262 video
+
+
+
+### h263p_mode
+
+
+
+
Description
+
+ Allows any client implementing the Sunshine codec extension to request H.263+ video streams.
+ Support is unadvertised by default and must be enabled here. Sunshine selects and probes its H.263+
+ backend independently of MPEG-2 and modern codecs; H.264 support is not a prerequisite.
+ @warning{H.263+ uses software encoding in Sunshine and may reduce host performance.}
+
+
+
+
Default
+
@code{}
+ 0
+ @endcode
+
+
+
Example
+
@code{}
+ h263p_mode = 2
+ @endcode
+
+
+
Choices
+
0
+
preserve modern codec automatic selection and keep H.263+ unadvertised
+
+
+
1
+
do not advertise support for H.263+
+
+
+
2
+
advertise support for H.263+ video
+
+
+
+### Legacy codec protocol
+
+MPEG-2 and H.263+ are opt-in Sunshine protocol extensions available to any implementing client. Both require
+8-bit 4:2:0 video. Sunshine advertises a codec only after its backend passes a probe; enabling an option does
+not guarantee that the codec is available on the host. Client automatic selection prefers AV1, HEVC, then H.264
+before the legacy codecs. A client requesting only one codec fails negotiation if that codec is unavailable.
+
+| Codec | `ServerCodecModeSupport` flag | RTSP DESCRIBE marker | `x-nv-vqos[0].bitStreamFormat` |
+|--------------|-------------------------------|----------------------|--------------------------------|
+| MPEG-2/H.262 | `0x00800000` | `MPV/90000` | `3` |
+| H.263+ | `0x02000000` | `H263-1998/90000` | `4` |
+
+The streams retain GameStream video framing, frame headers, recovery requests, and FEC. The SDP codec names do
+not imply RFC 2250 or RFC 4629 RTP payload packetization. Clients must implement the extension's negotiation,
+GameStream depacketization, and matching video decoder; changing a client name does not enable codec support.
+
### capture
diff --git a/src/config.cpp b/src/config.cpp
index de28fe97c6f..3115f136e7b 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -711,6 +711,8 @@ namespace config {
0, // hevc_mode
0, // av1_mode
+ 0, // mpeg2_mode
+ 0, // h263p_mode
2, // min_threads
{
@@ -1600,6 +1602,8 @@ namespace config {
int_f(vars, "qp", video.qp);
int_between_f(vars, "hevc_mode", video.hevc_mode, {0, 3});
int_between_f(vars, "av1_mode", video.av1_mode, {0, 3});
+ int_between_f(vars, "mpeg2_mode", video.mpeg2_mode, {0, 2});
+ int_between_f(vars, "h263p_mode", video.h263p_mode, {0, 2});
int_f(vars, "min_threads", video.min_threads);
string_f(vars, "sw_preset", video.sw.sw_preset);
if (!video.sw.sw_preset.empty()) {
diff --git a/src/config.h b/src/config.h
index ead9e9812db..5693beaa040 100644
--- a/src/config.h
+++ b/src/config.h
@@ -75,6 +75,8 @@ namespace config {
int hevc_mode; ///< HEVC support mode advertised to clients.
int av1_mode; ///< AV1 support mode advertised to clients.
+ int mpeg2_mode; ///< MPEG-2 codec advertisement mode.
+ int h263p_mode; ///< H.263+ codec advertisement mode.
int min_threads; ///< Minimum number of threads or slices for CPU encoding.
diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp
index bf1bf6a6c80..e84cf4e0a89 100644
--- a/src/nvhttp.cpp
+++ b/src/nvhttp.cpp
@@ -1201,8 +1201,8 @@ namespace nvhttp {
}
uint32_t get_codec_mode_flags() {
- uint32_t codec_mode_flags = SCM_H264;
- if (video::last_encoder_probe_supported_yuv444_for_codec[0]) {
+ uint32_t codec_mode_flags = video::last_encoder_probe_supported_h264 ? SCM_H264 : 0;
+ if (video::last_encoder_probe_supported_h264 && video::last_encoder_probe_supported_yuv444_for_codec[0]) {
codec_mode_flags |= SCM_H264_HIGH8_444;
}
if (video::active_hevc_mode >= 2) {
@@ -1230,6 +1230,12 @@ namespace nvhttp {
if ((video::active_av1_mode == 4 || video::active_av1_mode == 5) && video::last_encoder_probe_supported_dynamic_range_yuv444_for_codec[2]) {
codec_mode_flags |= SCM_AV1_HIGH10_444;
}
+ if (video::active_mpeg2_mode >= 2) {
+ codec_mode_flags |= SCM_MPEG2;
+ }
+ if (video::active_h263p_mode == 2) {
+ codec_mode_flags |= SCM_H263P;
+ }
return codec_mode_flags;
}
diff --git a/src/rtsp.cpp b/src/rtsp.cpp
index 4532c2b09c5..196b2be286e 100644
--- a/src/rtsp.cpp
+++ b/src/rtsp.cpp
@@ -28,6 +28,7 @@ extern "C" {
#include "input.h"
#include "logging.h"
#include "network.h"
+#include "nvhttp.h"
#include "rtsp.h"
#include "stream.h"
#include "sync.h"
@@ -41,6 +42,23 @@ using asio::ip::udp;
using namespace std::literals;
namespace rtsp_stream {
+ std::string describe_video_codecs(uint32_t codec_modes) {
+ std::string result;
+ if (codec_modes & SCM_MASK_HEVC) {
+ result += "sprop-parameter-sets=AAAAAU\r\n";
+ }
+ if (codec_modes & SCM_MASK_AV1) {
+ result += "a=rtpmap:98 AV1/90000\r\n";
+ }
+ if (codec_modes & SCM_MPEG2) {
+ result += "a=rtpmap:98 MPV/90000\r\n";
+ }
+ if (codec_modes & SCM_H263P) {
+ result += "a=rtpmap:98 H263-1998/90000\r\n";
+ }
+ return result;
+ }
+
/**
* @brief Release msg resources.
*
@@ -902,13 +920,7 @@ namespace rtsp_stream {
ss << "a=x-nv-video[0].refPicInvalidation:1"sv << std::endl;
}
- if (video::active_hevc_mode != 1) {
- ss << "sprop-parameter-sets=AAAAAU"sv << std::endl;
- }
-
- if (video::active_av1_mode != 1) {
- ss << "a=rtpmap:98 AV1/90000"sv << std::endl;
- }
+ ss << describe_video_codecs(nvhttp::get_codec_mode_flags());
if (!session.surround_params.empty()) {
// If we have our own surround parameters, advertise them twice first
@@ -1226,20 +1238,34 @@ namespace rtsp_stream {
config.monitor.bitrate = (int) configuredBitrateKbps;
}
- if (config.monitor.videoFormat == 1 && video::active_hevc_mode == 1) {
+ if (config.monitor.videoFormat == video::format_hevc && video::active_hevc_mode == 1) {
BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv;
respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
return;
}
- if (config.monitor.videoFormat == 2 && video::active_av1_mode == 1) {
+ if (config.monitor.videoFormat == video::format_av1 && video::active_av1_mode == 1) {
BOOST_LOG(warning) << "AV1 is disabled, yet the client requested AV1"sv;
respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
return;
}
+ if (config.monitor.videoFormat == video::format_mpeg2 && video::active_mpeg2_mode < 2) {
+ BOOST_LOG(warning) << "MPEG-2 is disabled, yet the client requested MPEG-2"sv;
+
+ respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
+ return;
+ }
+
+ if (config.monitor.videoFormat == video::format_h263p && video::active_h263p_mode != 2) {
+ BOOST_LOG(warning) << "H.263+ is disabled, yet the client requested H.263+"sv;
+
+ respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
+ return;
+ }
+
// Check that any required encryption is enabled
auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address());
if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY && (config.encryptionFlagsEnabled & (SS_ENC_VIDEO | SS_ENC_AUDIO)) != (SS_ENC_VIDEO | SS_ENC_AUDIO)) {
diff --git a/src/rtsp.h b/src/rtsp.h
index 108c881e200..bfbd7be6975 100644
--- a/src/rtsp.h
+++ b/src/rtsp.h
@@ -9,12 +9,21 @@
#include
#include
#include
+#include
// local includes
#include "crypto.h"
#include "thread_safe.h"
namespace rtsp_stream {
+ /**
+ * @brief Describe the codecs advertised by a host, independently of client identity.
+ *
+ * @param codec_modes Validated ServerCodecModeSupport capability bits.
+ * @return GameStream DESCRIBE codec attributes; these do not change RTP packetization.
+ */
+ std::string describe_video_codecs(uint32_t codec_modes);
+
constexpr auto RTSP_SETUP_PORT = 21; ///< GameStream base-port offset used for the RTSP setup listener.
#pragma pack(push, 1)
diff --git a/src/thread_safe.h b/src/thread_safe.h
index a2fc99e741d..6d183f23253 100644
--- a/src/thread_safe.h
+++ b/src/thread_safe.h
@@ -707,6 +707,7 @@ namespace safe {
if (!_count) {
new (_object_buf.data()) element_type;
if (_construct(*reinterpret_cast(_object_buf.data())) != 0) {
+ reinterpret_cast(_object_buf.data())->~element_type();
return ptr_t {nullptr};
}
}
@@ -722,7 +723,7 @@ namespace safe {
std::array _object_buf;
- std::uint32_t _count;
+ std::uint32_t _count = 0; ///< Number of active references, including dynamically allocated storage.
std::mutex _lock;
};
diff --git a/src/video.cpp b/src/video.cpp
index 995e8a73a1e..29805675ff9 100644
--- a/src/video.cpp
+++ b/src/video.cpp
@@ -36,6 +36,7 @@ extern "C" {
#include "platform/common.h"
#include "sync.h"
#include "video.h"
+#include "video_capture_groups.h"
#ifdef _WIN32
extern "C" {
@@ -76,6 +77,18 @@ namespace video {
#endif
}
+ namespace {
+ /**
+ * @brief Identify the legacy video formats supported for compatibility clients.
+ *
+ * @param video_format Requested video_format_e identifier.
+ * @return Whether the format is MPEG-2 or H.263+.
+ */
+ bool is_legacy_video_format(int video_format) {
+ return video_format == format_mpeg2 || video_format == format_h263p;
+ }
+ } // namespace
+
/**
* @brief Release context resources.
*/
@@ -698,6 +711,7 @@ namespace video {
safe::mail_raw_t::event_t touch_port_events; ///< Event carrying updated touch viewport metadata.
config_t config; ///< Stream or encoder configuration captured for the worker.
+ const encoder_t *encoder_p; ///< Encoder selected for this stream's video format.
int frame_nr; ///< Next capture-frame number assigned to encoded packets.
stream::session_t *channel_data; ///< Stream session forwarded to packet senders.
};
@@ -725,6 +739,7 @@ namespace video {
struct capture_ctx_t {
img_event_t images; ///< Queue of captured images waiting for encode.
config_t config; ///< Stream or encoder configuration captured for the worker.
+ const encoder_t *encoder_p; ///< Encoder selected for this stream's video format.
};
/**
@@ -735,7 +750,6 @@ namespace video {
std::jthread capture_thread; ///< Capture thread.
safe::signal_t reinit_event; ///< Reinit event.
- const encoder_t *encoder_p; ///< Encoder p.
sync_util::sync_t> display_wp; ///< Display wp.
};
@@ -744,21 +758,11 @@ namespace video {
*/
struct capture_thread_sync_ctx_t {
encode_session_ctx_queue_t encode_session_ctx_queue {30}; ///< Encode session ctx queue.
+ std::jthread capture_thread; ///< Worker owned by this encoder's capture group.
};
- /**
- * @brief Start the synchronous multi-client capture thread.
- *
- * @param ctx Unused state required by the shared capture lifetime callback signature.
- * @return 0 when the capture thread is started.
- */
- int start_capture_sync(capture_thread_sync_ctx_t &ctx);
- /**
- * @brief Leave synchronous capture cleanup to the capture thread.
- *
- * @param ctx Unused state required by the shared capture lifetime callback signature.
- */
- void end_capture_sync(capture_thread_sync_ctx_t &ctx);
+ int start_capture_sync(capture_thread_sync_ctx_t &context);
+ void end_capture_sync(capture_thread_sync_ctx_t &context);
/**
* @brief Start the asynchronous capture thread.
*
@@ -774,8 +778,35 @@ namespace video {
void end_capture_async(capture_thread_async_ctx_t &ctx);
// Keep a reference counter to ensure the capture thread only runs when other threads have a reference to the capture thread
- auto capture_thread_async = safe::make_shared(start_capture_async, end_capture_async); ///< Capture thread async.
- auto capture_thread_sync = safe::make_shared(start_capture_sync, end_capture_sync); ///< Capture thread sync.
+ display_switch_state_t display_switches; ///< Broadcast monitor selection to all encoder capture groups.
+ capture_group_pool_t capture_threads_async {
+ [](auto &context) {
+ display_switches.start_worker();
+ const auto status = start_capture_async(context);
+ if (status != 0) {
+ display_switches.stop_worker();
+ }
+ return status;
+ },
+ [](auto &context) {
+ end_capture_async(context);
+ display_switches.stop_worker();
+ },
+ }; ///< Capture workers grouped by encoder backend.
+ capture_group_pool_t capture_threads_sync {
+ [](auto &context) {
+ display_switches.start_worker();
+ const auto status = start_capture_sync(context);
+ if (status != 0) {
+ display_switches.stop_worker();
+ }
+ return status;
+ },
+ [](auto &context) {
+ end_capture_sync(context);
+ display_switches.stop_worker();
+ },
+ }; ///< Synchronous workers grouped by encoder backend.
#ifdef _WIN32
/**
@@ -820,6 +851,8 @@ namespace video {
"h264_nvenc"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | YUV444_SUPPORT | ASYNC_TEARDOWN // flags
};
#elif !defined(__APPLE__)
@@ -922,6 +955,8 @@ namespace video {
"h264_nvenc"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING | YUV444_SUPPORT
};
#endif
@@ -1038,6 +1073,27 @@ namespace video {
"h264_qsv"s,
{}, // capabilities
},
+ {
+ // Common options
+ {
+ {"preset"s, &config::video.qsv.qsv_preset},
+ {"forced_idr"s, 1},
+ {"async_depth"s, 1},
+ {"low_delay_brc"s, 1},
+ {"low_power"s, 1},
+ },
+ {}, // SDR-specific options
+ {}, // HDR-specific options
+ {}, // YUV444 SDR-specific options
+ {}, // YUV444 HDR-specific options
+ {
+ // Fallback options
+ {"low_power"s, 0},
+ },
+ "mpeg2_qsv"s,
+ {}, // capabilities
+ },
+ {}, // H.263+
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT
};
@@ -1152,6 +1208,8 @@ namespace video {
"h264_amf"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING
};
@@ -1215,6 +1273,8 @@ namespace video {
"h264_mf"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING | FIXED_GOP_SIZE // MF encoder doesn't support on-demand IDR frames
};
#endif
@@ -1292,6 +1352,32 @@ namespace video {
"libx264"s,
{}, // capabilities
},
+ {
+ {
+ // Required when AV_CODEC_FLAG_CLOSED_GOP is set.
+ {"sc_threshold"s, 1000000000},
+ },
+ {}, // SDR-specific options
+ {}, // HDR-specific options
+ {}, // YUV444 SDR-specific options
+ {}, // YUV444 HDR-specific options
+ {}, // Fallback options
+ "mpeg2video"s,
+ {}, // capabilities
+ },
+ {
+ {
+ // Required when AV_CODEC_FLAG_CLOSED_GOP is set.
+ {"sc_threshold"s, 1000000000},
+ },
+ {}, // SDR-specific options
+ {}, // HDR-specific options
+ {}, // YUV444 SDR-specific options
+ {}, // YUV444 HDR-specific options
+ {}, // Fallback options
+ "h263p"s,
+ {}, // capabilities
+ },
H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT
};
@@ -1358,6 +1444,21 @@ namespace video {
"h264_vaapi"s,
{}, // capabilities
},
+ {
+ // Common options
+ {
+ {"async_depth"s, 1},
+ {"idr_interval"s, std::numeric_limits::max()},
+ },
+ {}, // SDR-specific options
+ {}, // HDR-specific options
+ {}, // YUV444 SDR-specific options
+ {}, // YUV444 HDR-specific options
+ {}, // Fallback options
+ "mpeg2_vaapi"s,
+ {}, // capabilities
+ },
+ {}, // H.263+
// RC buffer size will be set in platform code if supported
LIMITED_GOP_SIZE | PARALLEL_ENCODING | NO_RC_BUF_LIMIT
};
@@ -1432,6 +1533,8 @@ namespace video {
"h264_vulkan"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
LIMITED_GOP_SIZE | PARALLEL_ENCODING
};
#endif // SUNSHINE_BUILD_VULKAN
@@ -1483,6 +1586,8 @@ namespace video {
"h264_vt"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING
};
#endif
@@ -1509,13 +1614,54 @@ namespace video {
};
static encoder_t *chosen_encoder;
+ static std::array codec_encoders {}; ///< Independently selected backend for each codec.
+ bool last_encoder_probe_supported_h264 = false; ///< Whether the modern encoder passed its baseline H.264 probe.
int active_hevc_mode; ///< HEVC mode selected by the most recent encoder probe.
int active_av1_mode; ///< AV1 mode selected by the most recent encoder probe.
+ int active_mpeg2_mode; ///< MPEG-2 mode selected by the most recent encoder probe.
+ int active_h263p_mode; ///< H.263+ mode selected by the most recent encoder probe.
bool last_encoder_probe_supported_ref_frames_invalidation = false; ///< Whether the last probe found reference-frame invalidation support.
std::array last_encoder_probe_supported_yuv444_for_codec = {}; ///< YUV444 support discovered for each probed codec.
std::array last_encoder_probe_supported_dynamic_range_for_codec = {}; ///< 10-bit 4:2:0 support discovered for each probed codec.
std::array last_encoder_probe_supported_dynamic_range_yuv444_for_codec = {}; ///< 10-bit YUV444 support discovered for each probed codec.
+ /**
+ * @brief Check whether an encoder passed validation for a requested video format.
+ *
+ * @param encoder Encoder whose codec capabilities should be checked.
+ * @param video_format Requested video_format_e identifier.
+ * @return Whether the encoder supports the requested video format.
+ */
+ bool encoder_supports_video_format(const encoder_t &encoder, int video_format) {
+ switch (video_format) {
+ case format_mpeg2:
+ return encoder.mpeg2[encoder_t::PASSED];
+ case format_h263p:
+ return encoder.h263p[encoder_t::PASSED];
+ case format_h264:
+ return encoder.h264[encoder_t::PASSED];
+ case format_hevc:
+ return encoder.hevc[encoder_t::PASSED];
+ case format_av1:
+ return encoder.av1[encoder_t::PASSED];
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * @brief Select the encoder for a modern or legacy video stream.
+ *
+ * @param config Video configuration requested by the client.
+ * @return Selected encoder, or nullptr when encoder probing found no suitable backend.
+ */
+ encoder_t *encoder_for_config(const config_t &config) {
+ if (config.videoFormat < 0 || config.videoFormat >= VIDEO_FORMAT_COUNT) {
+ return nullptr;
+ }
+ return codec_encoders[config.videoFormat];
+ }
+
/**
* @brief Recreate a display capture object after a capture failure.
*
@@ -1599,13 +1745,11 @@ namespace video {
* @param capture_ctx_queue Capture context queue.
* @param display_wp Weak pointer holder for the active display.
* @param reinit_event Signal raised while the display is being reinitialized.
- * @param encoder Selected encoder.
*/
void captureThread(
std::shared_ptr> capture_ctx_queue,
sync_util::sync_t> &display_wp,
- safe::signal_t &reinit_event,
- const encoder_t &encoder
+ safe::signal_t &reinit_event
) {
std::vector capture_ctxs;
@@ -1622,6 +1766,7 @@ namespace video {
});
auto switch_display_event = mail::man->event(mail::switch_display);
+ auto display_request = display_switches.poll(switch_display_event);
// Wait for the initial capture context or a request to stop the queue
auto initial_capture_ctx = capture_ctx_queue->pop();
@@ -1629,12 +1774,16 @@ namespace video {
return;
}
capture_ctxs.emplace_back(std::move(*initial_capture_ctx));
+ const auto &encoder = *capture_ctxs.front().encoder_p;
// Get all the monitor names now, rather than at boot, to
// get the most up-to-date list available monitors
std::vector display_names;
int display_p = -1;
refresh_displays(encoder.platform_formats->dev_type, display_names, display_p);
+ if (display_request.index) {
+ display_p = std::clamp(*display_request.index, 0, static_cast(display_names.size()) - 1);
+ }
auto disp = platf::display(encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config);
if (!disp) {
return;
@@ -1745,7 +1894,7 @@ namespace video {
while (capture_ctx_queue->running()) {
bool artificial_reinit = false;
- auto push_captured_image_callback = [&capture_ctxs, &capture_ctx_queue, &switch_display_event, &artificial_reinit](std::shared_ptr &&img, bool frame_captured) {
+ auto push_captured_image_callback = [&capture_ctxs, &capture_ctx_queue, &switch_display_event, &display_request, &artificial_reinit](std::shared_ptr &&img, bool frame_captured) {
KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), {
if (!capture_ctx->images->running()) {
capture_ctx = capture_ctxs.erase(capture_ctx);
@@ -1768,7 +1917,7 @@ namespace video {
capture_ctxs.emplace_back(std::move(*capture_ctx_queue->pop()));
}
- if (switch_display_event->peek()) {
+ if (display_switches.poll(switch_display_event).generation != display_request.generation) {
artificial_reinit = true;
return false;
}
@@ -1826,8 +1975,9 @@ namespace video {
refresh_displays(encoder.platform_formats->dev_type, display_names, display_p);
// Process any pending display switch with the new list of displays
- if (switch_display_event->peek()) {
- display_p = std::clamp(*switch_display_event->pop(), 0, static_cast(display_names.size()) - 1);
+ display_request = display_switches.poll(switch_display_event);
+ if (display_request.index) {
+ display_p = std::clamp(*display_request.index, 0, static_cast(display_names.size()) - 1);
}
// reset_display() will sleep between retries
@@ -2074,7 +2224,7 @@ namespace video {
bool hardware = platform_formats->avcodec_base_dev_type != AV_HWDEVICE_TYPE_NONE;
auto &video_format = encoder.codec_from_config(config);
- if (!video_format[encoder_t::PASSED] || !disp->is_codec_supported(video_format.name, config)) {
+ if (video_format.name.empty() || !video_format[encoder_t::PASSED] || !disp->is_codec_supported(video_format.name, config)) {
BOOST_LOG(error) << encoder.name << ": "sv << video_format.name << " mode not supported"sv;
return nullptr;
}
@@ -2110,6 +2260,9 @@ namespace video {
(colorspace.bit_depth == 10 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_10bit :
(colorspace.bit_depth == 10 && config.chromaSamplingType == 1) ? platform_formats->avcodec_pix_fmt_yuv444_10bit :
AV_PIX_FMT_NONE;
+ if (!hardware && is_legacy_video_format(config.videoFormat)) {
+ sw_fmt = AV_PIX_FMT_YUV420P;
+ }
// Allow up to 1 retry to apply the set of fallback options.
//
@@ -2126,13 +2279,13 @@ namespace video {
ctx->time_base = AVRational {fps.den, fps.num};
switch (config.videoFormat) {
- case 0:
+ case format_h264:
// 10-bit h264 encoding is not supported by our streaming protocol
assert(!config.dynamicRange);
ctx->profile = select_h264_profile(video_format.name, config, config::video.amd.amd_coder);
break;
- case 1:
+ case format_hevc:
if (config.chromaSamplingType == 1) {
// HEVC uses the same RExt profile for both 8 and 10 bit YUV 4:4:4 encoding
ctx->profile = AV_PROFILE_HEVC_REXT;
@@ -2141,23 +2294,33 @@ namespace video {
}
break;
- case 2:
+ case format_av1:
// AV1 supports both 8 and 10 bit encoding with the same Main profile
// but YUV 4:4:4 sampling requires High profile
ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_AV1_HIGH : AV_PROFILE_AV1_MAIN;
break;
+
+ case format_mpeg2:
+ ctx->profile = AV_PROFILE_MPEG2_MAIN;
+ break;
+
+ case format_h263p:
+ break;
}
// B-frames delay decoder output, so never use them
ctx->max_b_frames = 0;
- // Use an infinite GOP length since I-frames are generated on demand
- // Exception: encoders with FIXED_GOP_SIZE flag don't support on-demand IDR
- if (encoder.flags & FIXED_GOP_SIZE) {
- // Fixed GOP for encoders that don't support on-demand IDR (e.g. Media Foundation)
+ if (is_legacy_video_format(config.videoFormat)) {
+ // FFmpeg's legacy encoders reject the infinite GOP value used by modern codecs.
+ ctx->gop_size = 600;
+ ctx->keyint_min = 1;
+ } else if (encoder.flags & FIXED_GOP_SIZE) {
+ // Fixed GOP for encoders that don't support on-demand IDR (e.g. Media Foundation).
ctx->gop_size = 120; // ~2 seconds at 60 FPS - larger to reduce oversized IDR frame frequency
ctx->keyint_min = 120;
} else {
+ // Use an infinite GOP length since I-frames are generated on demand.
ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ?
std::numeric_limits::max() :
std::numeric_limits::max();
@@ -2323,12 +2486,14 @@ namespace video {
ctx->rc_min_rate = bitrate;
}
- if (encoder.flags & RELAXED_COMPLIANCE) {
+ if ((encoder.flags & RELAXED_COMPLIANCE) || config.videoFormat == format_h263p) {
ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
}
if (!(encoder.flags & NO_RC_BUF_LIMIT)) {
- if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) {
+ if (is_legacy_video_format(config.videoFormat)) {
+ ctx->rc_buffer_size = bitrate;
+ } else if (!hardware && (ctx->slices > 1 || config.videoFormat == format_hevc)) {
// Use a larger rc_buffer_size for software encoding when slices are enabled,
// because libx264 can severely degrade quality if the buffer is too small.
// libx265 encounters this issue more frequently, so always scale the
@@ -2798,11 +2963,10 @@ namespace video {
std::vector &display_names,
int &display_p
) {
- const auto &encoder = *chosen_encoder;
-
std::shared_ptr disp;
auto switch_display_event = mail::man->event(mail::switch_display);
+ auto display_request = display_switches.poll(switch_display_event);
if (synced_session_ctxs.empty()) {
auto ctx = encode_session_ctx_queue.pop();
@@ -2813,13 +2977,16 @@ namespace video {
synced_session_ctxs.emplace_back(std::make_unique(std::move(*ctx)));
}
+ const auto &encoder = *synced_session_ctxs.front()->encoder_p;
+
while (encode_session_ctx_queue.running()) {
// Refresh display names since a display removal might have caused the reinitialization
refresh_displays(encoder.platform_formats->dev_type, display_names, display_p);
// Process any pending display switch with the new list of displays
- if (switch_display_event->peek()) {
- display_p = std::clamp(*switch_display_event->pop(), 0, static_cast(display_names.size()) - 1);
+ display_request = display_switches.poll(switch_display_event);
+ if (display_request.index) {
+ display_p = std::clamp(*display_request.index, 0, static_cast(display_names.size()) - 1);
}
// reset_display() will sleep between retries
@@ -2915,7 +3082,7 @@ namespace video {
++pos;
})
- if (switch_display_event->peek()) {
+ if (display_switches.poll(switch_display_event).generation != display_request.generation) {
ec = platf::capture_e::reinit;
return false;
}
@@ -2945,13 +3112,13 @@ namespace video {
/**
* @brief Run synchronous capture and encode work on the capture thread.
+ *
+ * @param capture_context Encoder-specific worker state kept alive by its stream handles.
*/
- void captureThreadSync() {
- auto ref = capture_thread_sync.ref();
-
+ void captureThreadSync(capture_thread_sync_ctx_t &capture_context) {
std::vector> synced_session_ctxs;
- auto &ctx = ref->encode_session_ctx_queue;
+ auto &ctx = capture_context.encode_session_ctx_queue;
auto lg = util::fail_guard([&ctx, &synced_session_ctxs]() {
ctx.stop();
@@ -2980,11 +3147,13 @@ namespace video {
*
* @param mail Session mail bus.
* @param config Video configuration.
+ * @param encoder Encoder selected for the requested video format.
* @param channel_data Stream session carried with each encoded packet.
*/
void capture_async(
safe::mail_t mail,
config_t &config,
+ const encoder_t &encoder,
stream::session_t *channel_data
) {
auto shutdown_event = mail->event(mail::shutdown);
@@ -2995,12 +3164,12 @@ namespace video {
shutdown_event->raise(true);
});
- auto ref = capture_thread_async.ref();
+ auto ref = capture_threads_async.ref(&encoder);
if (!ref) {
return;
}
- ref->capture_ctx_queue->raise(capture_ctx_t {images, config});
+ ref->capture_ctx_queue->raise(capture_ctx_t {images, config, &encoder});
if (!ref->capture_ctx_queue->running()) {
return;
@@ -3031,8 +3200,6 @@ namespace video {
display = ref->display_wp->lock();
}
- auto &encoder = *chosen_encoder;
-
auto encode_device = make_encode_device(*display, encoder, config);
if (!encode_device) {
return;
@@ -3060,7 +3227,7 @@ namespace video {
display,
std::move(encode_device),
ref->reinit_event,
- *ref->encoder_p,
+ encoder,
channel_data
);
}
@@ -3078,16 +3245,24 @@ namespace video {
config_t config,
stream::session_t *channel_data
) {
- config = resolve_dynamic_range(*chosen_encoder, config);
-
auto idr_events = mail->event(mail::idr);
+ auto encoder = encoder_for_config(config);
+ if (!encoder || !encoder_supports_video_format(*encoder, config.videoFormat)) {
+ BOOST_LOG(error) << "No encoder is available for video format ["sv << config.videoFormat << ']';
+ return;
+ }
+
+ config = resolve_dynamic_range(*encoder, config);
idr_events->raise(true);
- if (chosen_encoder->flags & PARALLEL_ENCODING) {
- capture_async(std::move(mail), config, channel_data);
+ if (encoder->flags & PARALLEL_ENCODING) {
+ capture_async(std::move(mail), config, *encoder, channel_data);
} else {
safe::signal_t join_event;
- auto ref = capture_thread_sync.ref();
+ auto ref = capture_threads_sync.ref(encoder);
+ if (!ref) {
+ return;
+ }
ref->encode_session_ctx_queue.raise(sync_session_ctx_t {
&join_event,
mail->event(mail::shutdown),
@@ -3096,6 +3271,7 @@ namespace video {
mail->event(mail::hdr),
mail->event(mail::touch_port),
config,
+ encoder,
1,
channel_data,
});
@@ -3172,6 +3348,46 @@ namespace video {
return flag;
}
+ bool validate_legacy_codec(encoder_t &encoder, int video_format) {
+ if (!is_legacy_video_format(video_format)) {
+ return false;
+ }
+ auto &codec = video_format == format_mpeg2 ? encoder.mpeg2 : encoder.h263p;
+ codec.capabilities.reset();
+ if (codec.name.empty() || !encoder.platform_formats) {
+ return false;
+ }
+
+ config_t config {1920, 1080, 60, 6000, 1000, 1, 1, 1, video_format, 0, 0, 0};
+ std::shared_ptr display;
+ reset_display(display, encoder.platform_formats->dev_type, display_device::map_output_name(config::video.output_name), config);
+ if (!display || !display->is_codec_supported(codec.name, config)) {
+ return false;
+ }
+
+ codec[encoder_t::PASSED] = true;
+ codec[encoder_t::REF_FRAMES_RESTRICT] = true;
+ const auto max_ref_frames = validate_config(display, encoder, config);
+ config.numRefFrames = 0;
+ const auto autoselect = max_ref_frames >= 0 ? max_ref_frames : validate_config(display, encoder, config);
+ codec.capabilities.reset();
+ codec[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames >= 0;
+ codec[encoder_t::PASSED] = autoselect >= 0;
+ return codec[encoder_t::PASSED];
+ }
+
+ encoder_t *select_legacy_encoder(std::span candidates, int video_format, const codec_probe_t &probe) {
+ if (!is_legacy_video_format(video_format)) {
+ return nullptr;
+ }
+ for (auto *encoder : candidates) {
+ if (encoder && probe(*encoder, video_format)) {
+ return encoder;
+ }
+ }
+ return nullptr;
+ }
+
/**
* @brief Validate encoder before it is used.
*/
@@ -3229,8 +3445,8 @@ namespace video {
encoder.h264[encoder_t::PASSED] = true;
if (test_hevc) {
- config_max_ref_frames.videoFormat = 1;
- config_autoselect.videoFormat = 1;
+ config_max_ref_frames.videoFormat = format_hevc;
+ config_autoselect.videoFormat = format_hevc;
if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) {
auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames);
@@ -3257,8 +3473,8 @@ namespace video {
}
if (test_av1) {
- config_max_ref_frames.videoFormat = 2;
- config_autoselect.videoFormat = 2;
+ config_max_ref_frames.videoFormat = format_av1;
+ config_autoselect.videoFormat = format_av1;
if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) {
auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames);
@@ -3351,12 +3567,12 @@ namespace video {
encoder.h264[encoder_t::DYNAMIC_RANGE] = false;
encoder.h264[encoder_t::DYNAMIC_RANGE_YUV444] = false;
- test_yuv444(encoder.hevc, 1);
- test_yuv420_hdr(encoder.hevc, 1);
- test_yuv444_hdr(encoder.hevc, 1);
- test_yuv444(encoder.av1, 2);
- test_yuv420_hdr(encoder.av1, 2);
- test_yuv444_hdr(encoder.av1, 2);
+ test_yuv444(encoder.hevc, format_hevc);
+ test_yuv420_hdr(encoder.hevc, format_hevc);
+ test_yuv444_hdr(encoder.hevc, format_hevc);
+ test_yuv444(encoder.av1, format_av1);
+ test_yuv420_hdr(encoder.av1, format_av1);
+ test_yuv444_hdr(encoder.av1, format_av1);
}
encoder.h264[encoder_t::VUI_PARAMETERS] = encoder.h264[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE];
@@ -3374,6 +3590,7 @@ namespace video {
}
void reset_encoder_probe_results() {
+ last_encoder_probe_supported_h264 = false;
last_encoder_probe_supported_ref_frames_invalidation = false;
last_encoder_probe_supported_yuv444_for_codec = {};
last_encoder_probe_supported_dynamic_range_for_codec = {};
@@ -3381,6 +3598,7 @@ namespace video {
}
void record_encoder_probe_results(const encoder_t &encoder) {
+ last_encoder_probe_supported_h264 = encoder.h264[encoder_t::PASSED];
last_encoder_probe_supported_ref_frames_invalidation = (encoder.flags & REF_FRAMES_INVALIDATION);
last_encoder_probe_supported_yuv444_for_codec[0] = encoder.h264[encoder_t::PASSED] &&
encoder.h264[encoder_t::YUV444];
@@ -3411,15 +3629,18 @@ namespace video {
auto encoder_list = encoders;
// If we already have a good encoder, check to see if another probe is required
- if (chosen_encoder && !(chosen_encoder->flags & ALWAYS_REPROBE) && !platf::needs_encoder_reenumeration()) {
+ if (chosen_encoder && !(chosen_encoder->flags & ALWAYS_REPROBE) && active_mpeg2_mode < 2 && active_h263p_mode != 2 && config::video.mpeg2_mode != 2 && config::video.h263p_mode != 2 && !platf::needs_encoder_reenumeration()) {
return 0;
}
// Restart encoder selection
auto previous_encoder = chosen_encoder;
chosen_encoder = nullptr;
+ codec_encoders.fill(nullptr);
active_hevc_mode = config::video.hevc_mode;
active_av1_mode = config::video.av1_mode;
+ active_mpeg2_mode = config::video.mpeg2_mode;
+ active_h263p_mode = config::video.h263p_mode;
reset_encoder_probe_results();
auto adjust_encoder_constraints_hevc = [&](encoder_t *encoder) {
@@ -3526,7 +3747,7 @@ namespace video {
});
if (chosen_encoder == nullptr) {
- BOOST_LOG(error) << "Couldn't find any working encoder that meets HEVC/AV1 requirements"sv;
+ BOOST_LOG(error) << "Couldn't find any working encoder that meets codec requirements"sv;
}
}
@@ -3553,7 +3774,40 @@ namespace video {
});
}
- if (chosen_encoder == nullptr) {
+ if (chosen_encoder) {
+ for (int format : {format_h264, format_hevc, format_av1}) {
+ if (encoder_supports_video_format(*chosen_encoder, format)) {
+ codec_encoders[format] = chosen_encoder;
+ }
+ }
+ }
+
+ // A failed modern probe must not remove a valid legacy encoder from consideration.
+ std::vector legacy_candidates(std::begin(encoders), std::end(encoders));
+ auto prioritize = [&](encoder_t *encoder) {
+ const auto position = std::find(legacy_candidates.begin(), legacy_candidates.end(), encoder);
+ if (position != legacy_candidates.end()) {
+ std::rotate(legacy_candidates.begin(), position, std::next(position));
+ }
+ };
+ prioritize(chosen_encoder);
+ for (auto *encoder : encoders) {
+ if (encoder->name == config::video.encoder) {
+ prioritize(encoder);
+ break;
+ }
+ }
+ for (const auto &[format, mode] : {std::pair {format_mpeg2, &active_mpeg2_mode}, std::pair {format_h263p, &active_h263p_mode}}) {
+ if (*mode == 2) {
+ codec_encoders[format] = select_legacy_encoder(legacy_candidates, format, validate_legacy_codec);
+ if (!codec_encoders[format]) {
+ BOOST_LOG(warning) << "No encoder supports legacy video format [" << format << ']';
+ *mode = 1;
+ }
+ }
+ }
+
+ if (!chosen_encoder && !codec_encoders[format_mpeg2] && !codec_encoders[format_h263p]) {
const auto output_name {display_device::map_output_name(config::video.output_name)};
BOOST_LOG(fatal) << "Unable to find display or encoder during startup."sv;
if (!config::video.adapter_name.empty() || !output_name.empty()) {
@@ -3568,71 +3822,92 @@ namespace video {
BOOST_LOG(info) << "// Ignore any errors mentioned above, they are not relevant. //"sv;
BOOST_LOG(info);
- auto &encoder = *chosen_encoder;
-
- record_encoder_probe_results(encoder);
+ if (chosen_encoder) {
+ auto &encoder = *chosen_encoder;
- BOOST_LOG(debug) << "------ h264 ------"sv;
- for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
- auto flag = static_cast(x);
- BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.h264[flag] ? ": supported"sv : ": unsupported"sv);
- }
- BOOST_LOG(debug) << "-------------------"sv;
- BOOST_LOG(info) << "Found H.264 encoder: "sv << encoder.h264.name << " ["sv << encoder.name << ']';
+ record_encoder_probe_results(encoder);
- if (encoder.hevc[encoder_t::PASSED]) {
- BOOST_LOG(debug) << "------ hevc ------"sv;
+ BOOST_LOG(debug) << "------ h264 ------"sv;
for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
auto flag = static_cast(x);
- BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.hevc[flag] ? ": supported"sv : ": unsupported"sv);
+ BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.h264[flag] ? ": supported"sv : ": unsupported"sv);
}
BOOST_LOG(debug) << "-------------------"sv;
+ BOOST_LOG(info) << "Found H.264 encoder: "sv << encoder.h264.name << " ["sv << encoder.name << ']';
- BOOST_LOG(info) << "Found HEVC encoder: "sv << encoder.hevc.name << " ["sv << encoder.name << ']';
- }
+ if (encoder.hevc[encoder_t::PASSED]) {
+ BOOST_LOG(debug) << "------ hevc ------"sv;
+ for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
+ auto flag = static_cast(x);
+ BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.hevc[flag] ? ": supported"sv : ": unsupported"sv);
+ }
+ BOOST_LOG(debug) << "-------------------"sv;
- if (encoder.av1[encoder_t::PASSED]) {
- BOOST_LOG(debug) << "------ av1 ------"sv;
- for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
- auto flag = static_cast(x);
- BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.av1[flag] ? ": supported"sv : ": unsupported"sv);
+ BOOST_LOG(info) << "Found HEVC encoder: "sv << encoder.hevc.name << " ["sv << encoder.name << ']';
}
- BOOST_LOG(debug) << "-------------------"sv;
- BOOST_LOG(info) << "Found AV1 encoder: "sv << encoder.av1.name << " ["sv << encoder.name << ']';
- }
+ if (encoder.av1[encoder_t::PASSED]) {
+ BOOST_LOG(debug) << "------ av1 ------"sv;
+ for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
+ auto flag = static_cast(x);
+ BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.av1[flag] ? ": supported"sv : ": unsupported"sv);
+ }
+ BOOST_LOG(debug) << "-------------------"sv;
- // 2 - passed
- // 3 - HDR yuv420
- // 4 - HDR yuv444
- // 5 - HDR yuv420 & HDR yuv444
+ BOOST_LOG(info) << "Found AV1 encoder: "sv << encoder.av1.name << " ["sv << encoder.name << ']';
+ }
- if (active_hevc_mode == 0) {
- active_hevc_mode = 1;
- if (encoder.hevc[encoder_t::PASSED]) {
- active_hevc_mode = 2;
- if (encoder.hevc[encoder_t::DYNAMIC_RANGE]) {
- active_hevc_mode += 1;
+ // 2 - passed
+ // 3 - HDR yuv420
+ // 4 - HDR yuv444
+ // 5 - HDR yuv420 & HDR yuv444
+
+ if (active_hevc_mode == 0) {
+ active_hevc_mode = 1;
+ if (encoder.hevc[encoder_t::PASSED]) {
+ active_hevc_mode = 2;
+ if (encoder.hevc[encoder_t::DYNAMIC_RANGE]) {
+ active_hevc_mode += 1;
+ }
+ if (encoder.hevc[encoder_t::DYNAMIC_RANGE_YUV444]) {
+ active_hevc_mode += 2;
+ }
}
- if (encoder.hevc[encoder_t::DYNAMIC_RANGE_YUV444]) {
- active_hevc_mode += 2;
+ BOOST_LOG(debug) << "ENCODER STATUS ACTIVE_HEVC_MODE: "sv << active_hevc_mode;
+ }
+
+ if (active_av1_mode == 0) {
+ active_av1_mode = 1;
+ if (encoder.av1[encoder_t::PASSED]) {
+ active_av1_mode = 2;
+ if (encoder.av1[encoder_t::DYNAMIC_RANGE]) {
+ active_av1_mode += 1;
+ }
+ if (encoder.av1[encoder_t::DYNAMIC_RANGE_YUV444]) {
+ active_av1_mode += 2;
+ }
}
+ BOOST_LOG(debug) << "ENCODER STATUS ACTIVE_AV1_MODE: "sv << active_av1_mode;
}
- BOOST_LOG(debug) << "ENCODER STATUS ACTIVE_HEVC_MODE: "sv << active_hevc_mode;
- }
- if (active_av1_mode == 0) {
+ } else {
+ active_hevc_mode = 1;
active_av1_mode = 1;
- if (encoder.av1[encoder_t::PASSED]) {
- active_av1_mode = 2;
- if (encoder.av1[encoder_t::DYNAMIC_RANGE]) {
- active_av1_mode += 1;
- }
- if (encoder.av1[encoder_t::DYNAMIC_RANGE_YUV444]) {
- active_av1_mode += 2;
- }
+ }
+ for (int format : {format_mpeg2, format_h263p}) {
+ if (auto *backend = codec_encoders[format]) {
+ config_t config {};
+ config.videoFormat = format;
+ BOOST_LOG(info) << "Found legacy encoder: " << backend->codec_from_config(config).name << " [" << backend->name << ']';
}
- BOOST_LOG(debug) << "ENCODER STATUS ACTIVE_AV1_MODE: "sv << active_av1_mode;
+ }
+
+ if (active_mpeg2_mode == 0) {
+ active_mpeg2_mode = 1;
+ }
+
+ if (active_h263p_mode == 0) {
+ active_h263p_mode = 1;
}
return 0;
@@ -3760,7 +4035,6 @@ namespace video {
* @brief Start capture async.
*/
int start_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) {
- capture_thread_ctx.encoder_p = chosen_encoder;
capture_thread_ctx.reinit_event.reset();
capture_thread_ctx.capture_ctx_queue = std::make_shared>(30);
@@ -3769,8 +4043,7 @@ namespace video {
captureThread,
capture_thread_ctx.capture_ctx_queue,
std::ref(capture_thread_ctx.display_wp),
- std::ref(capture_thread_ctx.reinit_event),
- std::ref(*capture_thread_ctx.encoder_p)
+ std::ref(capture_thread_ctx.reinit_event)
};
return 0;
@@ -3786,19 +4059,24 @@ namespace video {
}
/**
- * @brief Start capture sync.
- * @details The shared lifetime callback supplies unused state; the thread accesses the shared capture queue directly.
+ * @brief Start the synchronous worker for one encoder capture group.
+ *
+ * @param context Encoder-specific queue and owned worker thread.
+ * @return Zero after starting the worker.
*/
- int start_capture_sync(capture_thread_sync_ctx_t &) {
- std::jthread {&captureThreadSync}.detach();
+ int start_capture_sync(capture_thread_sync_ctx_t &context) {
+ context.capture_thread = std::jthread {captureThreadSync, std::ref(context)};
return 0;
}
/**
- * @brief Leave synchronous capture cleanup to the capture thread.
+ * @brief Stop and join a synchronous group before destroying its queue.
+ *
+ * @param context Encoder-specific worker whose last stream has left.
*/
- void end_capture_sync(capture_thread_sync_ctx_t &) {
- // The capture thread stops its queue and signals its sessions when it exits.
+ void end_capture_sync(capture_thread_sync_ctx_t &context) {
+ context.encode_session_ctx_queue.stop();
+ context.capture_thread.join();
}
/**
diff --git a/src/video.h b/src/video.h
index 8b2158b6137..a969e7fd457 100644
--- a/src/video.h
+++ b/src/video.h
@@ -7,7 +7,9 @@
// standard includes
#include
#include
+#include
#include
+#include
#include
#include
#include
@@ -34,6 +36,18 @@ namespace stream {
}
namespace video {
+ /**
+ * @brief Video stream format identifiers used by GameStream setup.
+ */
+ enum video_format_e {
+ format_h264 = 0, ///< H.264 High profile.
+ format_hevc = 1, ///< HEVC Main/Main10 profile.
+ format_av1 = 2, ///< AV1 Main/Main10 profile.
+ format_mpeg2 = 3, ///< MPEG-2/H.262 video.
+ format_h263p = 4, ///< H.263+ video.
+ };
+
+ constexpr int VIDEO_FORMAT_COUNT = format_h263p + 1; ///< Number of supported wire codec identifiers.
/**
* @brief Encoding configuration requested by a remote client.
@@ -47,7 +61,7 @@ namespace video {
int slicesPerFrame; ///< Number of slices per frame.
int numRefFrames; ///< Maximum number of reference frames.
int encoderCscMode; ///< Requested color range and SDR colorspace; HDR always uses BT.2020 and ST2084.
- int videoFormat; ///< Video codec format: 0 = H.264, 1 = HEVC, 2 = AV1.
+ int videoFormat; ///< Video codec format selected from video_format_e.
int dynamicRange; ///< Encoding color depth: 0 = 8-bit, 1 = 10-bit.
int chromaSamplingType; ///< Chroma sampling type: 0 = 4:2:0, 1 = 4:4:4.
int enableIntraRefresh; ///< Intra refresh setting: 0 = disabled, 1 = enabled.
@@ -444,7 +458,7 @@ namespace video {
const std::unique_ptr platform_formats; ///< Platform-specific memory and pixel formats accepted by the encoder.
/**
- * @brief Codec capabilities for AV1, HEVC, or H.264.
+ * @brief Codec capabilities for a modern or legacy video format.
*/
struct codec_t {
std::vector common_options; ///< Options applied to every encode mode for this codec.
@@ -481,24 +495,30 @@ namespace video {
codec_t av1; ///< AV1 codec capability and option set.
codec_t hevc; ///< HEVC codec capability and option set.
codec_t h264; ///< H.264 codec capability and option set.
+ codec_t mpeg2; ///< MPEG-2 codec capability and option set.
+ codec_t h263p; ///< H.263+ codec capability and option set.
/**
* @brief Select the codec descriptor requested by a stream configuration.
*
* @param config Configuration values to apply.
- * @return Codec descriptor for H.264, HEVC, or AV1.
+ * @return Codec descriptor for H.264, HEVC, AV1, MPEG-2, or H.263+.
*/
const codec_t &codec_from_config(const config_t &config) const {
switch (config.videoFormat) {
default:
BOOST_LOG(error) << "Unknown video format " << config.videoFormat << ", falling back to H.264";
// fallthrough
- case 0:
+ case format_h264:
return h264;
- case 1:
+ case format_hevc:
return hevc;
- case 2:
+ case format_av1:
return av1;
+ case format_mpeg2:
+ return mpeg2;
+ case format_h263p:
+ return h263p;
}
}
@@ -765,8 +785,35 @@ namespace video {
*/
using hdr_info_t = std::unique_ptr;
- extern int active_hevc_mode;
- extern int active_av1_mode;
+ /**
+ * @brief Callback used to probe one encoder for one requested legacy codec.
+ */
+ using codec_probe_t = std::function;
+
+ /**
+ * @brief Probe MPEG-2 or H.263+ independently of H.264 support.
+ *
+ * @param encoder Backend whose requested codec should be validated.
+ * @param video_format MPEG-2 or H.263+ wire codec identifier.
+ * @return Whether the backend successfully encoded the requested legacy codec.
+ */
+ bool validate_legacy_codec(encoder_t &encoder, int video_format);
+
+ /**
+ * @brief Choose the first backend that passes a codec-specific probe.
+ *
+ * @param candidates Backend preference order, including software fallbacks.
+ * @param video_format Requested legacy codec identifier.
+ * @param probe Callback validating only the requested codec.
+ * @return Selected backend, or nullptr when no candidate passes.
+ */
+ encoder_t *select_legacy_encoder(std::span candidates, int video_format, const codec_probe_t &probe);
+
+ extern bool last_encoder_probe_supported_h264; ///< Whether baseline H.264 is available.
+ extern int active_hevc_mode; ///< Active HEVC codec advertisement mode.
+ extern int active_av1_mode; ///< Active AV1 codec advertisement mode.
+ extern int active_mpeg2_mode; ///< Active MPEG-2 codec advertisement mode.
+ extern int active_h263p_mode; ///< Active H.263+ codec advertisement mode.
extern bool last_encoder_probe_supported_ref_frames_invalidation;
extern std::array last_encoder_probe_supported_yuv444_for_codec; // 0 - H.264, 1 - HEVC, 2 - AV1
extern std::array last_encoder_probe_supported_dynamic_range_for_codec; // 0 - H.264, 1 - HEVC, 2 - AV1
diff --git a/src/video_capture_groups.h b/src/video_capture_groups.h
new file mode 100644
index 00000000000..56a3a5ee1eb
--- /dev/null
+++ b/src/video_capture_groups.h
@@ -0,0 +1,119 @@
+/**
+ * @file src/video_capture_groups.h
+ * @brief Share capture workers only between streams using the same encoder backend.
+ */
+#pragma once
+
+// standard includes
+#include
+#include