From 9f4419d7068ece5165614bbc62d24673f39553c4 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Mon, 25 May 2026 12:29:45 -0400 Subject: [PATCH] feat(video): add support for legacy MPEG-2 and H.263+ encoders --- docs/configuration.md | 79 +++++ src/config.cpp | 4 + src/config.h | 6 +- src/nvhttp.cpp | 6 + src/rtsp.cpp | 26 +- src/video.cpp | 301 ++++++++++++++++-- src/video.h | 30 +- src_assets/common/assets/web/config.html | 2 + .../assets/web/configs/tabs/Advanced.vue | 22 ++ .../assets/web/public/assets/locale/en.json | 10 + tests/unit/test_video.cpp | 47 +++ third-party/moonlight-common-c | 2 +- 12 files changed, 492 insertions(+), 43 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e9fe004ddde..10ad3013fc1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2124,6 +2124,85 @@ editing the `conf` file in a text editor. Use the examples as reference. +### mpeg2_mode + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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. + @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
Choices0preserve modern codec automatic selection and keep MPEG-2 unadvertised
1do not advertise support for MPEG-2
2advertise 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
Choices0preserve modern codec automatic selection and keep H.263+ unadvertised
1do not advertise support for H.263+
2advertise support for H.263+ video
+ ### capture diff --git a/src/config.cpp b/src/config.cpp index 6d266c0ef22..7931f919fac 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -454,6 +454,8 @@ namespace config { 0, // hevc_mode 0, // av1_mode + 0, // mpeg2_mode + 0, // h263p_mode 2, // min_threads { @@ -1101,6 +1103,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 e0e40501d0b..45dd1e63a82 100644 --- a/src/config.h +++ b/src/config.h @@ -36,8 +36,10 @@ namespace config { // ffmpeg params int qp; // higher == more compression and less quality - int hevc_mode; - int av1_mode; + int hevc_mode; ///< HEVC codec advertisement mode. + int av1_mode; ///< AV1 codec advertisement mode. + int mpeg2_mode; ///< MPEG-2 codec advertisement mode. + int h263p_mode; ///< H.263+ codec advertisement mode. int min_threads; // Minimum number of threads/slices for CPU encoding diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 72e9dc5cd05..97fb28e65c6 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -763,6 +763,12 @@ namespace nvhttp { 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; + } tree.put("root.ServerCodecModeSupport", codec_mode_flags); if (!config::nvhttp.external_ip.empty()) { diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 0953cd59f60..ce2e5294686 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -815,6 +815,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; @@ -1115,20 +1123,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 && diff --git a/src/video.cpp b/src/video.cpp index 00af66c3a69..2beb8277b35 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -70,6 +70,10 @@ namespace video { BOOST_LOG(error) << "No display devices are active at the moment! Cannot probe the encoders."; return false; } + + bool is_legacy_video_format(int video_format) { + return video_format == format_mpeg2 || video_format == format_h263p; + } } // namespace void free_ctx(AVCodecContext *ctx) { @@ -437,6 +441,7 @@ namespace video { safe::mail_raw_t::event_t touch_port_events; config_t config; + const encoder_t *encoder_p; int frame_nr; void *channel_data; }; @@ -452,6 +457,7 @@ namespace video { struct capture_ctx_t { img_event_t images; config_t config; + const encoder_t *encoder_p; }; struct capture_thread_async_ctx_t { @@ -459,7 +465,6 @@ namespace video { std::thread capture_thread; safe::signal_t reinit_event; - const encoder_t *encoder_p; sync_util::sync_t> display_wp; }; @@ -513,6 +518,8 @@ namespace video { {}, // Fallback options "h264_nvenc"s, }, + {}, // MPEG-2 + {}, // H.263+ PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | YUV444_SUPPORT | ASYNC_TEARDOWN // flags }; #elif !defined(__APPLE__) @@ -610,6 +617,8 @@ namespace video { {}, // Fallback options "h264_nvenc"s, }, + {}, // MPEG-2 + {}, // H.263+ PARALLEL_ENCODING }; #endif @@ -720,6 +729,26 @@ namespace video { }, "h264_qsv"s, }, + { + // 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 }; @@ -826,6 +855,8 @@ namespace video { }, "h264_amf"s, }, + {}, // MPEG-2 + {}, // H.263+ PARALLEL_ENCODING }; @@ -883,6 +914,8 @@ namespace video { {}, // Fallback options "h264_mf"s, }, + {}, // MPEG-2 + {}, // H.263+ PARALLEL_ENCODING | FIXED_GOP_SIZE // MF encoder doesn't support on-demand IDR frames }; #endif @@ -954,6 +987,30 @@ namespace video { {}, // Fallback options "libx264"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 + "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 }; @@ -1011,6 +1068,20 @@ namespace video { {}, // Fallback options "h264_vaapi"s, }, + { + // 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 }; @@ -1082,6 +1153,8 @@ namespace video { {}, // Fallback options "h264_vulkan"s, }, + {}, // MPEG-2 + {}, // H.263+ LIMITED_GOP_SIZE | PARALLEL_ENCODING }; #endif // SUNSHINE_BUILD_VULKAN @@ -1151,6 +1224,8 @@ namespace video { }, "h264_videotoolbox"s, }, + {}, // MPEG-2 + {}, // H.263+ DEFAULT }; #endif @@ -1177,11 +1252,39 @@ namespace video { }; static encoder_t *chosen_encoder; + static encoder_t *legacy_encoder; int active_hevc_mode; int active_av1_mode; + int active_mpeg2_mode; + int active_h263p_mode; bool last_encoder_probe_supported_ref_frames_invalidation = false; std::array last_encoder_probe_supported_yuv444_for_codec = {}; + 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; + } + } + + 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; + } + void reset_display(std::shared_ptr &disp, const platf::mem_type_e &type, const std::string &display_name, const config_t &config) { // We try this twice, in case we still get an error on reinitialization for (int x = 0; x < 2; ++x) { @@ -1253,8 +1356,7 @@ namespace video { 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; @@ -1278,6 +1380,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 @@ -1627,7 +1730,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; } @@ -1655,6 +1758,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. // @@ -1675,13 +1781,13 @@ namespace video { } switch (config.videoFormat) { - case 0: + case format_h264: // 10-bit h264 encoding is not supported by our streaming protocol assert(!config.dynamicRange); ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_H264_HIGH_444_PREDICTIVE : AV_PROFILE_H264_HIGH; 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; @@ -1690,23 +1796,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(); @@ -1872,12 +1988,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 @@ -2268,8 +2386,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); @@ -2283,6 +2399,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); @@ -2445,6 +2563,7 @@ namespace video { void capture_async( safe::mail_t mail, config_t &config, + const encoder_t &encoder, void *channel_data ) { auto shutdown_event = mail->event(mail::shutdown); @@ -2460,7 +2579,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; @@ -2491,8 +2610,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; @@ -2520,7 +2637,7 @@ namespace video { display, std::move(encode_device), ref->reinit_event, - *ref->encoder_p, + encoder, channel_data ); } @@ -2532,10 +2649,15 @@ namespace video { void *channel_data ) { 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; + } 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(); @@ -2547,6 +2669,7 @@ namespace video { mail->event(mail::hdr), mail->event(mail::touch_port), config, + encoder, 1, channel_data, }); @@ -2623,10 +2746,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}; @@ -2666,8 +2793,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); @@ -2694,8 +2821,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); @@ -2721,6 +2848,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 { // H.264 is special because encoders may support YUV 4:4:4 without supporting 10-bit color depth @@ -2772,8 +2940,8 @@ namespace video { // HDR is not supported with H.264. Don't bother even trying it. encoder.h264[encoder_t::DYNAMIC_RANGE] = false; - test_hdr_and_yuv444(encoder.hevc, 1); - test_hdr_and_yuv444(encoder.av1, 2); + test_hdr_and_yuv444(encoder.hevc, format_hevc); + test_hdr_and_yuv444(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]; @@ -2806,8 +2974,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; last_encoder_probe_supported_ref_frames_invalidation = false; auto adjust_encoder_constraints = [&](encoder_t *encoder) { @@ -2829,6 +3000,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), { @@ -2870,7 +3059,8 @@ namespace video { } // Skip it if it doesn't support the specified codec at all - if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) || (active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) { + if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) || + (active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) { pos++; continue; } @@ -2886,7 +3076,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; } } @@ -2923,6 +3113,39 @@ 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); @@ -2967,6 +3190,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 << ']'; + } + if (active_hevc_mode == 0) { active_hevc_mode = encoder.hevc[encoder_t::PASSED] ? (encoder.hevc[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; } @@ -2975,6 +3206,14 @@ namespace video { active_av1_mode = encoder.av1[encoder_t::PASSED] ? (encoder.av1[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; } + if (active_mpeg2_mode == 0) { + active_mpeg2_mode = 1; + } + + if (active_h263p_mode == 0) { + active_h263p_mode = 1; + } + return 0; } @@ -3103,7 +3342,6 @@ namespace video { #endif 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); @@ -3112,8 +3350,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 8fa25850036..957382b142e 100644 --- a/src/video.h +++ b/src/video.h @@ -18,6 +18,16 @@ extern "C" { struct AVPacket; 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. + }; /* Encoding configuration requested by remote client */ struct config_t { @@ -34,7 +44,7 @@ namespace video { SDR encoding colorspace (encoderCscMode >> 1) : 0 - BT.601, 1 - BT.709, 2 - BT.2020 */ int encoderCscMode; - int videoFormat; // 0 - H.264, 1 - HEVC, 2 - AV1 + int videoFormat; // video_format_e /* Encoding color depth (bit depth): 0 - 8-bit, 1 - 10-bit HDR encoding activates when color depth is higher than 8-bit and the display which is being captured is operating in HDR mode */ @@ -189,18 +199,24 @@ namespace video { codec_t av1; codec_t hevc; codec_t h264; + codec_t mpeg2; + codec_t h263p; 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; } } @@ -341,8 +357,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 diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 27d84205dc4..2f301997832 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -277,6 +277,8 @@

{{ $t('config.configuration') }}

"min_threads": 2, "hevc_mode": 0, "av1_mode": 0, + "mpeg2_mode": 0, + "h263p_mode": 0, "capture": "", "encoder": "", }, diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue index 108d2b3362d..cf5c80ea78b 100644 --- a/src_assets/common/assets/web/configs/tabs/Advanced.vue +++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue @@ -58,6 +58,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 f4eee2cdccf..0d4be84d811 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -243,6 +243,11 @@ "gamepad_xone": "XOne (Xbox One)", "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", @@ -283,6 +288,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_video.cpp b/tests/unit/test_video.cpp index ed578f7d8db..f017cafe15f 100644 --- a/tests/unit/test_video.cpp +++ b/tests/unit/test_video.cpp @@ -6,6 +6,29 @@ #include +namespace { + struct ActiveVideoModeGuard { + 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} { + } + + ~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; + int av1_mode; + int mpeg2_mode; + int h263p_mode; + }; +} // namespace + struct EncoderTest: PlatformTestSuite, testing::WithParamInterface { void SetUp() override { auto &encoder = *GetParam(); @@ -49,6 +72,30 @@ TEST_P(EncoderTest, ValidateEncoder) { // todo:: test something besides fixture setup } +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]); +} + +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: testing::TestWithParam> {}; TEST_P(FramerateX100Test, Run) { diff --git a/third-party/moonlight-common-c b/third-party/moonlight-common-c index 2600beaf13f..ab1b6c776f1 160000 --- a/third-party/moonlight-common-c +++ b/third-party/moonlight-common-c @@ -1 +1 @@ -Subproject commit 2600beaf13f18bfa43453609cf5e3b84a4227760 +Subproject commit ab1b6c776f13e621e8ad7c3b04de096f132ad5d9