+ Allows compatibility clients to request MPEG-2/H.262 video streams. Automatic mode keeps this legacy codec
+ below H.264, HEVC, and AV1 by not advertising it unless explicitly forced. When forced, Sunshine may use a
+ legacy-capable fallback encoder for MPEG-2 streams without changing the encoder used for modern codecs.
+ @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 compatibility clients to request H.263+ video streams. Automatic mode keeps this legacy codec below
+ H.264, HEVC, and AV1 by not advertising it unless explicitly forced. When forced, Sunshine may use a
+ legacy-capable fallback encoder for H.263+ streams without changing the encoder used for modern codecs.
+ @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
+
+
+
### 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..5e60ba99e6f 100644
--- a/src/nvhttp.cpp
+++ b/src/nvhttp.cpp
@@ -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..619e3d180dd 100644
--- a/src/rtsp.cpp
+++ b/src/rtsp.cpp
@@ -910,6 +910,14 @@ namespace rtsp_stream {
ss << "a=rtpmap:98 AV1/90000"sv << std::endl;
}
+ if (video::active_mpeg2_mode >= 2) {
+ ss << "a=rtpmap:98 MPV/90000"sv << std::endl;
+ }
+
+ if (video::active_h263p_mode == 2) {
+ ss << "a=rtpmap:98 H263-1998/90000"sv << std::endl;
+ }
+
if (!session.surround_params.empty()) {
// If we have our own surround parameters, advertise them twice first
ss << "a=fmtp:97 surround-params="sv << session.surround_params << std::endl;
@@ -1226,20 +1234,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/video.cpp b/src/video.cpp
index 995e8a73a1e..a3a5b10c8d3 100644
--- a/src/video.cpp
+++ b/src/video.cpp
@@ -76,6 +76,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 +710,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 +738,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 +749,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.
};
@@ -820,6 +833,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 +937,8 @@ namespace video {
"h264_nvenc"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING | YUV444_SUPPORT
};
#endif
@@ -1038,6 +1055,26 @@ 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,
+ },
+ {}, // H.263+
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT
};
@@ -1152,6 +1189,8 @@ namespace video {
"h264_amf"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING
};
@@ -1215,6 +1254,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 +1333,30 @@ 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,
+ },
+ {
+ {
+ // 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,
+ },
H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT
};
@@ -1358,6 +1423,20 @@ 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,
+ },
+ {}, // H.263+
// RC buffer size will be set in platform code if supported
LIMITED_GOP_SIZE | PARALLEL_ENCODING | NO_RC_BUF_LIMIT
};
@@ -1432,6 +1511,8 @@ namespace video {
"h264_vulkan"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
LIMITED_GOP_SIZE | PARALLEL_ENCODING
};
#endif // SUNSHINE_BUILD_VULKAN
@@ -1483,6 +1564,8 @@ namespace video {
"h264_vt"s,
{}, // capabilities
},
+ {}, // MPEG-2
+ {}, // H.263+
PARALLEL_ENCODING
};
#endif
@@ -1509,13 +1592,54 @@ namespace video {
};
static encoder_t *chosen_encoder;
+ static encoder_t *legacy_encoder; ///< Encoder selected for legacy compatibility streams.
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 (is_legacy_video_format(config.videoFormat) && legacy_encoder && encoder_supports_video_format(*legacy_encoder, config.videoFormat)) {
+ return legacy_encoder;
+ }
+
+ return chosen_encoder;
+ }
+
/**
* @brief Recreate a display capture object after a capture failure.
*
@@ -1599,13 +1723,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;
@@ -1629,6 +1751,7 @@ 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
@@ -2074,7 +2197,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 +2233,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 +2252,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 +2267,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 +2459,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,8 +2936,6 @@ 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);
@@ -2813,6 +2949,8 @@ 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);
@@ -2980,11 +3118,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);
@@ -3000,7 +3140,7 @@ namespace video {
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 +3171,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 +3198,7 @@ namespace video {
display,
std::move(encode_device),
ref->reinit_event,
- *ref->encoder_p,
+ encoder,
channel_data
);
}
@@ -3078,13 +3216,18 @@ 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();
@@ -3096,6 +3239,7 @@ namespace video {
mail->event(mail::hdr),
mail->event(mail::touch_port),
config,
+ encoder,
1,
channel_data,
});
@@ -3186,10 +3330,14 @@ namespace video {
auto test_hevc = active_hevc_mode >= 2 || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY));
auto test_av1 = active_av1_mode >= 2 || (active_av1_mode == 0 && !(encoder.flags & H264_ONLY));
+ auto test_mpeg2 = active_mpeg2_mode >= 2;
+ auto test_h263p = active_h263p_mode == 2;
encoder.h264.capabilities.set();
encoder.hevc.capabilities.set();
encoder.av1.capabilities.set();
+ encoder.mpeg2.capabilities.set();
+ encoder.h263p.capabilities.set();
// First, test encoder viability
config_t config_max_ref_frames {1920, 1080, 60, 6000, 1000, 1, 1, 1, 0, 0, 0, 0};
@@ -3229,8 +3377,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 +3405,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);
@@ -3284,6 +3432,47 @@ namespace video {
encoder.av1.capabilities.reset();
}
+ auto test_legacy_codec = [&](encoder_t::codec_t &codec, int video_format) {
+ config_max_ref_frames.videoFormat = video_format;
+ config_autoselect.videoFormat = video_format;
+ config_max_ref_frames.dynamicRange = 0;
+ config_autoselect.dynamicRange = 0;
+ config_max_ref_frames.chromaSamplingType = 0;
+ config_autoselect.chromaSamplingType = 0;
+ codec.capabilities.reset();
+ if (codec.name.empty()) {
+ return;
+ }
+
+ codec.capabilities.set();
+ if (disp->is_codec_supported(codec.name, config_autoselect)) {
+ auto max_ref_frames = validate_config(disp, encoder, config_max_ref_frames);
+
+ // Legacy codecs are only used for explicit compatibility paths, so a
+ // successful autoselect configuration is good enough if ref limits fail.
+ auto autoselect = max_ref_frames >= 0 ? max_ref_frames : validate_config(disp, encoder, config_autoselect);
+
+ codec.capabilities.reset();
+ codec[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames >= 0;
+ codec[encoder_t::PASSED] = max_ref_frames >= 0 || autoselect >= 0;
+ } else {
+ codec.capabilities.reset();
+ BOOST_LOG(info) << "Encoder ["sv << codec.name << "] is not supported on this GPU"sv;
+ }
+ };
+
+ if (test_mpeg2) {
+ test_legacy_codec(encoder.mpeg2, format_mpeg2);
+ } else {
+ encoder.mpeg2.capabilities.reset();
+ }
+
+ if (test_h263p) {
+ test_legacy_codec(encoder.h263p, format_h263p);
+ } else {
+ encoder.h263p.capabilities.reset();
+ }
+
// Test HDR and YUV444 support
{
auto test_yuv444 = [&](auto &flag_map, auto video_format) {
@@ -3351,12 +3540,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];
@@ -3418,8 +3607,11 @@ namespace video {
// Restart encoder selection
auto previous_encoder = chosen_encoder;
chosen_encoder = nullptr;
+ legacy_encoder = 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) {
@@ -3456,6 +3648,24 @@ namespace video {
}
};
+ auto adjust_legacy_encoder_constraints = [&](encoder_t *encoder) {
+ if (active_mpeg2_mode == 2 && !encoder->mpeg2[encoder_t::PASSED]) {
+ BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support MPEG-2 on this system"sv;
+ active_mpeg2_mode = 1;
+ }
+
+ if (active_h263p_mode == 2 && !encoder->h263p[encoder_t::PASSED]) {
+ BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support H.263+ on this system"sv;
+ active_h263p_mode = 1;
+ }
+ };
+
+ auto encoder_supports_requested_legacy_codecs = [&](encoder_t *encoder) {
+ return encoder &&
+ (active_mpeg2_mode < 2 || encoder->mpeg2[encoder_t::PASSED]) &&
+ (active_h263p_mode != 2 || encoder->h263p[encoder_t::PASSED]);
+ };
+
if (!config::video.encoder.empty()) {
// If there is a specific encoder specified, use it if it passes validation
KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {
@@ -3526,7 +3736,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;
}
}
@@ -3564,6 +3774,38 @@ namespace video {
return -1;
}
+ if (active_mpeg2_mode >= 2 || active_h263p_mode == 2) {
+ if (encoder_supports_requested_legacy_codecs(chosen_encoder)) {
+ legacy_encoder = chosen_encoder;
+ } else {
+ auto saved_hevc_mode = active_hevc_mode;
+ auto saved_av1_mode = active_av1_mode;
+ active_hevc_mode = 1;
+ active_av1_mode = 1;
+
+ if (validate_encoder(software, previous_encoder && previous_encoder != &software) && encoder_supports_requested_legacy_codecs(&software)) {
+ legacy_encoder = &software;
+ }
+
+ active_hevc_mode = saved_hevc_mode;
+ active_av1_mode = saved_av1_mode;
+ }
+
+ if (legacy_encoder) {
+ adjust_legacy_encoder_constraints(legacy_encoder);
+ } else {
+ if (active_mpeg2_mode == 2) {
+ BOOST_LOG(warning) << "No encoder supports MPEG-2 on this system"sv;
+ active_mpeg2_mode = 1;
+ }
+
+ if (active_h263p_mode == 2) {
+ BOOST_LOG(warning) << "No encoder supports H.263+ on this system"sv;
+ active_h263p_mode = 1;
+ }
+ }
+ }
+
BOOST_LOG(info);
BOOST_LOG(info) << "// Ignore any errors mentioned above, they are not relevant. //"sv;
BOOST_LOG(info);
@@ -3602,6 +3844,14 @@ namespace video {
BOOST_LOG(info) << "Found AV1 encoder: "sv << encoder.av1.name << " ["sv << encoder.name << ']';
}
+ if (legacy_encoder && legacy_encoder->mpeg2[encoder_t::PASSED]) {
+ BOOST_LOG(info) << "Found MPEG-2 encoder: "sv << legacy_encoder->mpeg2.name << " ["sv << legacy_encoder->name << ']';
+ }
+
+ if (legacy_encoder && legacy_encoder->h263p[encoder_t::PASSED]) {
+ BOOST_LOG(info) << "Found H.263+ encoder: "sv << legacy_encoder->h263p.name << " ["sv << legacy_encoder->name << ']';
+ }
+
// 2 - passed
// 3 - HDR yuv420
// 4 - HDR yuv444
@@ -3635,6 +3885,14 @@ namespace video {
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 +4018,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 +4026,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;
diff --git a/src/video.h b/src/video.h
index 8b2158b6137..aa9cfdeabf0 100644
--- a/src/video.h
+++ b/src/video.h
@@ -34,6 +34,16 @@ 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.
+ };
/**
* @brief Encoding configuration requested by a remote client.
@@ -47,7 +57,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.
@@ -481,24 +491,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 +781,10 @@ namespace video {
*/
using hdr_info_t = std::unique_ptr;
- extern int active_hevc_mode;
- extern int active_av1_mode;
+ 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_assets/common/assets/web/configs/config_tabs.json b/src_assets/common/assets/web/configs/config_tabs.json
index b1940b33d82..d15dd506f14 100644
--- a/src_assets/common/assets/web/configs/config_tabs.json
+++ b/src_assets/common/assets/web/configs/config_tabs.json
@@ -102,6 +102,8 @@
"min_threads": 2,
"hevc_mode": 0,
"av1_mode": 0,
+ "mpeg2_mode": 0,
+ "h263p_mode": 0,
"capture": "",
"fp16_sdr_gamma_encoded": "disabled",
"encoder": ""
diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue
index 17a2b8ead24..5d04b731eec 100644
--- a/src_assets/common/assets/web/configs/tabs/Advanced.vue
+++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue
@@ -59,6 +59,28 @@ const config = ref(props.config)
{{ $t('config.av1_mode_desc') }}
+
+
+
+
+
{{ $t('config.mpeg2_mode_desc') }}
+
+
+
+
+
+
+
{{ $t('config.h263p_mode_desc') }}
+
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 23f5baa943c..4cda1635adc 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -283,6 +283,11 @@
"gamepad_xseries": "Xbox Series",
"global_prep_cmd": "Command Preparations",
"global_prep_cmd_desc": "Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.",
+ "h263p_mode": "H.263+ Support",
+ "h263p_mode_0": "Sunshine will preserve modern codec automatic selection and keep H.263+ unadvertised",
+ "h263p_mode_1": "Sunshine will not advertise support for H.263+",
+ "h263p_mode_2": "Sunshine will advertise support for H.263+ video",
+ "h263p_mode_desc": "Allows compatibility clients to request H.263+ video streams. This codec is lower priority than H.264, HEVC, and AV1, and may use a legacy fallback encoder without changing the encoder used for modern codecs.",
"hevc_mode": "HEVC Support",
"hevc_mode_0": "Sunshine will advertise support for HEVC based on encoder capabilities (recommended)",
"hevc_mode_1": "Sunshine will not advertise support for HEVC",
@@ -333,6 +338,11 @@
"min_log_level_5": "Fatal",
"min_log_level_6": "None",
"min_log_level_desc": "The minimum log level printed to standard out",
+ "mpeg2_mode": "MPEG-2 Support",
+ "mpeg2_mode_0": "Sunshine will preserve modern codec automatic selection and keep MPEG-2 unadvertised",
+ "mpeg2_mode_1": "Sunshine will not advertise support for MPEG-2",
+ "mpeg2_mode_2": "Sunshine will advertise support for MPEG-2/H.262 video",
+ "mpeg2_mode_desc": "Allows compatibility clients to request MPEG-2/H.262 video streams. This codec is lower priority than H.264, HEVC, and AV1, and may use a legacy fallback encoder without changing the encoder used for modern codecs.",
"min_threads": "Minimum CPU Thread Count",
"min_threads_desc": "Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode at your desired streaming settings on your hardware.",
"misc": "Miscellaneous options",
diff --git a/tests/unit/test_nvhttp_codec_flags.cpp b/tests/unit/test_nvhttp_codec_flags.cpp
index 2c6336ebfdc..ad7354b0ff0 100644
--- a/tests/unit/test_nvhttp_codec_flags.cpp
+++ b/tests/unit/test_nvhttp_codec_flags.cpp
@@ -89,7 +89,7 @@ using CodecProbeAdvertisementTest = CodecProbeStateTest;
TEST_F(CodecProbeAdvertisementTest, Probed8Bit444Without10Bit444AdvertisesOnly8Bit444) {
using namespace std::literals;
- video::encoder_t encoder {"test"sv, {}, {}, {}, {}, 0};
+ video::encoder_t encoder {"test"sv, {}, {}, {}, {}, {}, {}, 0};
for (auto *codec : {&encoder.h264, &encoder.hevc, &encoder.av1}) {
(*codec)[video::encoder_t::PASSED] = true;
(*codec)[video::encoder_t::YUV444] = true;
diff --git a/tests/unit/test_video.cpp b/tests/unit/test_video.cpp
index 8bb2991d0da..07730442bf6 100644
--- a/tests/unit/test_video.cpp
+++ b/tests/unit/test_video.cpp
@@ -108,6 +108,58 @@ TEST(VideoFormatMappingTest, MapsSupportedPixelFormatsAndRejectsUnsupportedOnes)
}
}
+/**
+ * @brief Each supported video format selects its matching encoder codec descriptor.
+ */
+TEST(VideoCodecSelectionTest, SelectsModernAndLegacyCodecDescriptors) {
+ video::encoder_t encoder {};
+ const std::array cases {
+ std::pair {video::format_h264, &video::encoder_t::h264},
+ std::pair {video::format_hevc, &video::encoder_t::hevc},
+ std::pair {video::format_av1, &video::encoder_t::av1},
+ std::pair {video::format_mpeg2, &video::encoder_t::mpeg2},
+ std::pair {video::format_h263p, &video::encoder_t::h263p},
+ };
+
+ for (const auto &[format, codec] : cases) {
+ video::config_t config {};
+ config.videoFormat = format;
+ EXPECT_EQ(&encoder.codec_from_config(config), &(encoder.*codec)) << format;
+ }
+}
+
+namespace {
+ /**
+ * @brief Restore codec advertisement modes after tests temporarily override them.
+ */
+ struct ActiveVideoModeGuard {
+ /**
+ * @brief Save the active codec advertisement modes.
+ */
+ ActiveVideoModeGuard():
+ hevc_mode {video::active_hevc_mode},
+ av1_mode {video::active_av1_mode},
+ mpeg2_mode {video::active_mpeg2_mode},
+ h263p_mode {video::active_h263p_mode} {
+ }
+
+ /**
+ * @brief Restore the codec advertisement modes saved at construction.
+ */
+ ~ActiveVideoModeGuard() {
+ video::active_hevc_mode = hevc_mode;
+ video::active_av1_mode = av1_mode;
+ video::active_mpeg2_mode = mpeg2_mode;
+ video::active_h263p_mode = h263p_mode;
+ }
+
+ int hevc_mode; ///< Saved HEVC advertisement mode.
+ int av1_mode; ///< Saved AV1 advertisement mode.
+ int mpeg2_mode; ///< Saved MPEG-2 advertisement mode.
+ int h263p_mode; ///< Saved H.263+ advertisement mode.
+ };
+} // namespace
+
struct EncoderTest: PlatformTestSuite, testing::WithParamInterface {
void SetUp() override {
BaseTest::SetUp();
@@ -223,6 +275,8 @@ TEST_P(DynamicRangeTest, Resolve) {
{},
{},
{},
+ {},
+ {},
0,
};
encoder.h264.name = "h264_test";
@@ -284,7 +338,7 @@ struct EncoderProbeResultsTest: testing::Test {
};
TEST_F(EncoderProbeResultsTest, FailedCodecProbeDoesNotPublishCapabilities) {
- video::encoder_t encoder {"test"sv, {}, {}, {}, {}, 0};
+ video::encoder_t encoder {"test"sv, {}, {}, {}, {}, {}, {}, 0};
for (auto *codec : {&encoder.h264, &encoder.hevc, &encoder.av1}) {
(*codec)[video::encoder_t::PASSED] = false;
(*codec)[video::encoder_t::YUV444] = true;
@@ -303,7 +357,7 @@ TEST_F(EncoderProbeResultsTest, FailedCodecProbeDoesNotPublishCapabilities) {
TEST_F(EncoderProbeResultsTest, SubsequentProbeReplacesPriorResults) {
constexpr uint32_t ref_frames_invalidation = 1 << 8; // Mirrors REF_FRAMES_INVALIDATION in video.cpp.
- video::encoder_t first {"first"sv, {}, {}, {}, {}, ref_frames_invalidation};
+ video::encoder_t first {"first"sv, {}, {}, {}, {}, {}, {}, ref_frames_invalidation};
for (auto *codec : {&first.h264, &first.hevc, &first.av1}) {
(*codec)[video::encoder_t::PASSED] = true;
(*codec)[video::encoder_t::YUV444] = true;
@@ -312,7 +366,7 @@ TEST_F(EncoderProbeResultsTest, SubsequentProbeReplacesPriorResults) {
}
// The second encoder only supports plain 8-bit 4:2:0 H.264.
- video::encoder_t second {"second"sv, {}, {}, {}, {}, 0};
+ video::encoder_t second {"second"sv, {}, {}, {}, {}, {}, {}, 0};
second.h264[video::encoder_t::PASSED] = true;
video::record_encoder_probe_results(first);
@@ -422,6 +476,36 @@ INSTANTIATE_TEST_SUITE_P(
)
);
+/**
+ * @brief The software backend supports explicitly enabled MPEG-2 encoding.
+ */
+TEST_F(PlatformTestSuite, SoftwareEncoderSupportsForcedMpeg2) {
+ ActiveVideoModeGuard guard;
+
+ video::active_hevc_mode = 1;
+ video::active_av1_mode = 1;
+ video::active_mpeg2_mode = 2;
+ video::active_h263p_mode = 1;
+
+ ASSERT_TRUE(video::validate_encoder(video::software, false));
+ EXPECT_TRUE(video::software.mpeg2[video::encoder_t::PASSED]);
+}
+
+/**
+ * @brief The software backend supports explicitly enabled H.263+ encoding.
+ */
+TEST_F(PlatformTestSuite, SoftwareEncoderSupportsForcedH263Plus) {
+ ActiveVideoModeGuard guard;
+
+ video::active_hevc_mode = 1;
+ video::active_av1_mode = 1;
+ video::active_mpeg2_mode = 1;
+ video::active_h263p_mode = 2;
+
+ ASSERT_TRUE(video::validate_encoder(video::software, false));
+ EXPECT_TRUE(video::software.h263p[video::encoder_t::PASSED]);
+}
+
struct FramerateX100Test: BaseTest, testing::WithParamInterface> {};
TEST_P(FramerateX100Test, Run) {
diff --git a/tests/web/Configuration.test.js b/tests/web/Configuration.test.js
index 04f7a2aa127..78d2aa4210d 100644
--- a/tests/web/Configuration.test.js
+++ b/tests/web/Configuration.test.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
+import configTabs from '../../src_assets/common/assets/web/configs/config_tabs.json'
import DisplayDeviceOptions from '../../src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue'
import Advanced from '../../src_assets/common/assets/web/configs/tabs/Advanced.vue'
import General from '../../src_assets/common/assets/web/configs/tabs/General.vue'
@@ -133,6 +134,26 @@ describe('network configuration', () => {
})
})
+describe('legacy codec configuration', () => {
+ it.each(['mpeg2_mode', 'h263p_mode'])('loads the default and updates %s for each advertised mode', async key => {
+ const config = structuredClone(configTabs.find(tab => tab.id === 'advanced').options)
+ const wrapper = mount(Advanced, {
+ props: { platform: 'linux', config },
+ global: globalOptions,
+ })
+ const select = wrapper.get(`#${key}`)
+
+ expect(config[key]).toBe(0)
+ expect(select.element.value).toBe('0')
+ expect(select.findAll('option').map(option => option.attributes('value'))).toEqual(['0', '1', '2'])
+
+ for (const value of ['2', '1', '0']) {
+ await select.setValue(value)
+ expect(config[key]).toBe(value)
+ }
+ })
+})
+
describe('FEC configuration', () => {
it.each([0, 1, 20, 99, 100])('accepts %i percent and updates the configuration', async percentage => {
const config = { fec_percentage: 20 }
diff --git a/third-party/moonlight-common-c b/third-party/moonlight-common-c
index 94c65fd364d..d93e2877fb3 160000
--- a/third-party/moonlight-common-c
+++ b/third-party/moonlight-common-c
@@ -1 +1 @@
-Subproject commit 94c65fd364d17b223f00a8763c9a5d94a66cb8dd
+Subproject commit d93e2877fb36a31f0e2988418a06ed7515b1abfd
From d0047f80f847d2e4060411ff934993cc4acb0e6a Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 12 Nov 2026 22:36:55 -0500
Subject: [PATCH 2/5] Select legacy codec backends independently and isolate
capture groups
---
docs/configuration.md | 30 +-
src/nvhttp.cpp | 4 +-
src/rtsp.cpp | 34 +-
src/rtsp.h | 9 +
src/thread_safe.h | 3 +-
src/video.cpp | 406 +++++++++---------
src/video.h | 31 +-
src/video_capture_groups.h | 119 +++++
.../assets/web/public/assets/locale/en.json | 4 +-
tests/CMakeLists.txt | 4 +
tests/unit/test_legacy_codec_roundtrip.cpp | 218 ++++++++++
tests/unit/test_legacy_codec_selection.cpp | 80 ++++
tests/unit/test_nvhttp_codec_flags.cpp | 21 +
tests/unit/test_video.cpp | 18 +-
tests/unit/test_video_capture_groups.cpp | 144 +++++++
third-party/moonlight-common-c | 2 +-
16 files changed, 903 insertions(+), 224 deletions(-)
create mode 100644 src/video_capture_groups.h
create mode 100644 tests/unit/test_legacy_codec_roundtrip.cpp
create mode 100644 tests/unit/test_legacy_codec_selection.cpp
create mode 100644 tests/unit/test_video_capture_groups.cpp
diff --git a/docs/configuration.md b/docs/configuration.md
index be99c734aa7..c9f193f2de3 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -2258,9 +2258,11 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
Description
- Allows compatibility clients to request MPEG-2/H.262 video streams. Automatic mode keeps this legacy codec
- below H.264, HEVC, and AV1 by not advertising it unless explicitly forced. When forced, Sunshine may use a
- legacy-capable fallback encoder for MPEG-2 streams without changing the encoder used for modern codecs.
+ 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.}
@@ -2298,9 +2300,9 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
Description
- Allows compatibility clients to request H.263+ video streams. Automatic mode keeps this legacy codec below
- H.264, HEVC, and AV1 by not advertising it unless explicitly forced. When forced, Sunshine may use a
- legacy-capable fallback encoder for H.263+ streams without changing the encoder used for modern codecs.
+ 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.}
@@ -2331,6 +2333,22 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
+### 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/nvhttp.cpp b/src/nvhttp.cpp
index 5e60ba99e6f..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) {
diff --git a/src/rtsp.cpp b/src/rtsp.cpp
index 619e3d180dd..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,21 +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;
- }
-
- if (video::active_mpeg2_mode >= 2) {
- ss << "a=rtpmap:98 MPV/90000"sv << std::endl;
- }
-
- if (video::active_h263p_mode == 2) {
- ss << "a=rtpmap:98 H263-1998/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
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 a3a5b10c8d3..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" {
@@ -757,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.
*
@@ -787,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
/**
@@ -1073,6 +1091,7 @@ namespace video {
{"low_power"s, 0},
},
"mpeg2_qsv"s,
+ {}, // capabilities
},
{}, // H.263+
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT
@@ -1344,6 +1363,7 @@ namespace video {
{}, // YUV444 HDR-specific options
{}, // Fallback options
"mpeg2video"s,
+ {}, // capabilities
},
{
{
@@ -1356,6 +1376,7 @@ namespace video {
{}, // YUV444 HDR-specific options
{}, // Fallback options
"h263p"s,
+ {}, // capabilities
},
H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT
};
@@ -1435,6 +1456,7 @@ namespace video {
{}, // YUV444 HDR-specific options
{}, // Fallback options
"mpeg2_vaapi"s,
+ {}, // capabilities
},
{}, // H.263+
// RC buffer size will be set in platform code if supported
@@ -1592,7 +1614,8 @@ namespace video {
};
static encoder_t *chosen_encoder;
- static encoder_t *legacy_encoder; ///< Encoder selected for legacy compatibility streams.
+ 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.
@@ -1633,11 +1656,10 @@ namespace video {
* @return Selected encoder, or nullptr when encoder probing found no suitable backend.
*/
encoder_t *encoder_for_config(const config_t &config) {
- if (is_legacy_video_format(config.videoFormat) && legacy_encoder && encoder_supports_video_format(*legacy_encoder, config.videoFormat)) {
- return legacy_encoder;
+ if (config.videoFormat < 0 || config.videoFormat >= VIDEO_FORMAT_COUNT) {
+ return nullptr;
}
-
- return chosen_encoder;
+ return codec_encoders[config.videoFormat];
}
/**
@@ -1744,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();
@@ -1758,6 +1781,9 @@ namespace video {
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;
@@ -1868,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);
@@ -1891,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;
}
@@ -1949,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
@@ -2939,6 +2966,7 @@ namespace video {
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();
@@ -2956,8 +2984,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
@@ -3053,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;
}
@@ -3083,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();
@@ -3135,7 +3164,7 @@ namespace video {
shutdown_event->raise(true);
});
- auto ref = capture_thread_async.ref();
+ auto ref = capture_threads_async.ref(&encoder);
if (!ref) {
return;
}
@@ -3230,7 +3259,10 @@ namespace video {
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),
@@ -3316,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.
*/
@@ -3330,14 +3402,10 @@ namespace video {
auto test_hevc = active_hevc_mode >= 2 || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY));
auto test_av1 = active_av1_mode >= 2 || (active_av1_mode == 0 && !(encoder.flags & H264_ONLY));
- auto test_mpeg2 = active_mpeg2_mode >= 2;
- auto test_h263p = active_h263p_mode == 2;
encoder.h264.capabilities.set();
encoder.hevc.capabilities.set();
encoder.av1.capabilities.set();
- encoder.mpeg2.capabilities.set();
- encoder.h263p.capabilities.set();
// First, test encoder viability
config_t config_max_ref_frames {1920, 1080, 60, 6000, 1000, 1, 1, 1, 0, 0, 0, 0};
@@ -3432,47 +3500,6 @@ namespace video {
encoder.av1.capabilities.reset();
}
- auto test_legacy_codec = [&](encoder_t::codec_t &codec, int video_format) {
- config_max_ref_frames.videoFormat = video_format;
- config_autoselect.videoFormat = video_format;
- config_max_ref_frames.dynamicRange = 0;
- config_autoselect.dynamicRange = 0;
- config_max_ref_frames.chromaSamplingType = 0;
- config_autoselect.chromaSamplingType = 0;
- codec.capabilities.reset();
- if (codec.name.empty()) {
- return;
- }
-
- codec.capabilities.set();
- if (disp->is_codec_supported(codec.name, config_autoselect)) {
- auto max_ref_frames = validate_config(disp, encoder, config_max_ref_frames);
-
- // Legacy codecs are only used for explicit compatibility paths, so a
- // successful autoselect configuration is good enough if ref limits fail.
- auto autoselect = max_ref_frames >= 0 ? max_ref_frames : validate_config(disp, encoder, config_autoselect);
-
- codec.capabilities.reset();
- codec[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames >= 0;
- codec[encoder_t::PASSED] = max_ref_frames >= 0 || autoselect >= 0;
- } else {
- codec.capabilities.reset();
- BOOST_LOG(info) << "Encoder ["sv << codec.name << "] is not supported on this GPU"sv;
- }
- };
-
- if (test_mpeg2) {
- test_legacy_codec(encoder.mpeg2, format_mpeg2);
- } else {
- encoder.mpeg2.capabilities.reset();
- }
-
- if (test_h263p) {
- test_legacy_codec(encoder.h263p, format_h263p);
- } else {
- encoder.h263p.capabilities.reset();
- }
-
// Test HDR and YUV444 support
{
auto test_yuv444 = [&](auto &flag_map, auto video_format) {
@@ -3563,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 = {};
@@ -3570,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];
@@ -3600,14 +3629,14 @@ 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;
- legacy_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;
@@ -3648,24 +3677,6 @@ namespace video {
}
};
- auto adjust_legacy_encoder_constraints = [&](encoder_t *encoder) {
- if (active_mpeg2_mode == 2 && !encoder->mpeg2[encoder_t::PASSED]) {
- BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support MPEG-2 on this system"sv;
- active_mpeg2_mode = 1;
- }
-
- if (active_h263p_mode == 2 && !encoder->h263p[encoder_t::PASSED]) {
- BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support H.263+ on this system"sv;
- active_h263p_mode = 1;
- }
- };
-
- auto encoder_supports_requested_legacy_codecs = [&](encoder_t *encoder) {
- return encoder &&
- (active_mpeg2_mode < 2 || encoder->mpeg2[encoder_t::PASSED]) &&
- (active_h263p_mode != 2 || encoder->h263p[encoder_t::PASSED]);
- };
-
if (!config::video.encoder.empty()) {
// If there is a specific encoder specified, use it if it passes validation
KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {
@@ -3763,126 +3774,132 @@ namespace video {
});
}
- if (chosen_encoder == nullptr) {
- 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()) {
- BOOST_LOG(fatal) << "Please ensure your manually chosen GPU and monitor are connected and powered on."sv;
- } else {
- BOOST_LOG(fatal) << "Please check that a display is connected and powered on."sv;
+ 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;
+ }
}
- return -1;
}
- if (active_mpeg2_mode >= 2 || active_h263p_mode == 2) {
- if (encoder_supports_requested_legacy_codecs(chosen_encoder)) {
- legacy_encoder = chosen_encoder;
- } else {
- auto saved_hevc_mode = active_hevc_mode;
- auto saved_av1_mode = active_av1_mode;
- active_hevc_mode = 1;
- active_av1_mode = 1;
-
- if (validate_encoder(software, previous_encoder && previous_encoder != &software) && encoder_supports_requested_legacy_codecs(&software)) {
- legacy_encoder = &software;
+ // 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;
}
-
- active_hevc_mode = saved_hevc_mode;
- active_av1_mode = saved_av1_mode;
}
+ }
- if (legacy_encoder) {
- adjust_legacy_encoder_constraints(legacy_encoder);
+ 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()) {
+ BOOST_LOG(fatal) << "Please ensure your manually chosen GPU and monitor are connected and powered on."sv;
} else {
- if (active_mpeg2_mode == 2) {
- BOOST_LOG(warning) << "No encoder supports MPEG-2 on this system"sv;
- active_mpeg2_mode = 1;
- }
-
- if (active_h263p_mode == 2) {
- BOOST_LOG(warning) << "No encoder supports H.263+ on this system"sv;
- active_h263p_mode = 1;
- }
+ BOOST_LOG(fatal) << "Please check that a display is connected and powered on."sv;
}
+ return -1;
}
BOOST_LOG(info);
BOOST_LOG(info) << "// Ignore any errors mentioned above, they are not relevant. //"sv;
BOOST_LOG(info);
- auto &encoder = *chosen_encoder;
+ if (chosen_encoder) {
+ auto &encoder = *chosen_encoder;
- record_encoder_probe_results(encoder);
+ record_encoder_probe_results(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 << ']';
-
- 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 (legacy_encoder && legacy_encoder->mpeg2[encoder_t::PASSED]) {
- BOOST_LOG(info) << "Found MPEG-2 encoder: "sv << legacy_encoder->mpeg2.name << " ["sv << legacy_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;
- if (legacy_encoder && legacy_encoder->h263p[encoder_t::PASSED]) {
- BOOST_LOG(info) << "Found H.263+ encoder: "sv << legacy_encoder->h263p.name << " ["sv << legacy_encoder->name << ']';
- }
+ BOOST_LOG(info) << "Found AV1 encoder: "sv << encoder.av1.name << " ["sv << encoder.name << ']';
+ }
- // 2 - passed
- // 3 - HDR yuv420
- // 4 - HDR yuv444
- // 5 - HDR yuv420 & HDR yuv444
+ // 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 (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) {
@@ -4042,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 aa9cfdeabf0..a969e7fd457 100644
--- a/src/video.h
+++ b/src/video.h
@@ -7,7 +7,9 @@
// standard includes
#include
#include
+#include
#include
+#include
#include
#include
#include
@@ -45,6 +47,8 @@ namespace 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.
*/
@@ -454,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.
@@ -781,6 +785,31 @@ namespace video {
*/
using hdr_info_t = std::unique_ptr;
+ /**
+ * @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.
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