Skip to content

Commit 5a2991d

Browse files
feat: Add video decoder selection and MPEG/H263 support
Introduce a VideoDecoderSelection setting and UI to cycle/choose preferred decoder (Auto, H.264, MPEG-2, H.263+). Persist and parse the new streaming.video_decoder setting, include it in settings initialization/persistence, and expose the choice in the settings UI and status messages. Advertise supported Moonlight video formats based on the preference and pass that to the session startup; update the FFmpeg backend to select and log the appropriate decoder (mpeg2/h263/h264), improve error messages, and expose the active decoder name to stats/overlays. Enable required FFmpeg parsers/decoders and update the ffmpeg profile signature; add/update unit tests and bump the moonlight-common-c submodule.
1 parent b6387e1 commit 5a2991d

19 files changed

Lines changed: 375 additions & 26 deletions

cmake/moonlight-dependencies.cmake

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function(_moonlight_compute_ffmpeg_signature out_var nxdk_dir ffmpeg_source_dir)
8282
set(signature_inputs
8383
"FFMPEG_REVISION=${ffmpeg_revision}"
8484
"NXDK_DIR=${nxdk_dir}"
85-
"FFMPEG_PROFILE=h264-opus-xbox"
85+
"FFMPEG_PROFILE=h264-mpeg2-h263-opus-xbox"
8686
"FFMPEG_TARGET_OS=none"
8787
"FFMPEG_ARCH=x86"
8888
"FFMPEG_CC_WRAPPER_SHA256=${ffmpeg_cc_wrapper_hash}"
@@ -201,7 +201,13 @@ function(_moonlight_get_ffmpeg_configure_args out_var)
201201
--enable-swscale
202202
--enable-swresample
203203
--enable-parser=h264
204+
--enable-parser=h263
205+
--enable-parser=mpegvideo
204206
--enable-decoder=h264
207+
--enable-decoder=h263
208+
--enable-decoder=h263i
209+
--enable-decoder=h263p
210+
--enable-decoder=mpeg2video
205211
--enable-decoder=opus)
206212

207213
set(${out_var} "${ffmpeg_configure_args}" PARENT_SCOPE)

src/app/client_state.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ namespace {
3232
constexpr int DEFAULT_STREAM_BITRATE_KBPS = 1000;
3333
constexpr std::array<int, 6> STREAM_FRAMERATE_OPTIONS {15, 20, 24, 25, 30, 60};
3434
constexpr std::array<int, 9> STREAM_BITRATE_OPTIONS {500, 750, 1000, 1500, 2000, 2500, 3000, 4000, 5000};
35+
constexpr std::array<app::VideoDecoderSelection, 4> VIDEO_DECODER_OPTIONS {
36+
app::VideoDecoderSelection::autoDetect,
37+
app::VideoDecoderSelection::h264,
38+
app::VideoDecoderSelection::mpeg2,
39+
app::VideoDecoderSelection::h263p,
40+
};
3541

3642
/**
3743
* @brief Describes the keypad characters available for the active add-host field.
@@ -144,6 +150,27 @@ namespace {
144150
return std::to_string(videoMode.width) + "x" + std::to_string(videoMode.height);
145151
}
146152

153+
/**
154+
* @brief Return the settings label for one video decoder preference.
155+
*
156+
* @param selection Decoder preference to describe.
157+
* @return User-facing decoder preference label.
158+
*/
159+
const char *video_decoder_selection_label(app::VideoDecoderSelection selection) {
160+
switch (selection) {
161+
case app::VideoDecoderSelection::autoDetect:
162+
return "Auto";
163+
case app::VideoDecoderSelection::h264:
164+
return "H.264";
165+
case app::VideoDecoderSelection::mpeg2:
166+
return "MPEG-2/H.262";
167+
case app::VideoDecoderSelection::h263p:
168+
return "H.263+";
169+
}
170+
171+
return "Auto";
172+
}
173+
147174
/**
148175
* @brief Return the selected stream-resolution index inside the detected mode list.
149176
*
@@ -213,6 +240,22 @@ namespace {
213240
state.settings.streamBitrateKbps = STREAM_BITRATE_OPTIONS[nextIndex];
214241
}
215242

243+
/**
244+
* @brief Advance the preferred video decoder to the next supported option.
245+
*
246+
* @param state Current client state containing the preferred decoder.
247+
*/
248+
void cycle_video_decoder(app::ClientState &state) {
249+
const auto current = std::find(VIDEO_DECODER_OPTIONS.begin(), VIDEO_DECODER_OPTIONS.end(), state.settings.videoDecoder);
250+
if (current == VIDEO_DECODER_OPTIONS.end()) {
251+
state.settings.videoDecoder = app::VideoDecoderSelection::autoDetect;
252+
return;
253+
}
254+
255+
const std::size_t nextIndex = (static_cast<std::size_t>(std::distance(VIDEO_DECODER_OPTIONS.begin(), current)) + 1U) % VIDEO_DECODER_OPTIONS.size();
256+
state.settings.videoDecoder = VIDEO_DECODER_OPTIONS[nextIndex];
257+
}
258+
216259
std::string pairing_reset_endpoint_key(std::string_view address, uint16_t port) {
217260
return app::normalize_ipv4_address(address) + ":" + std::to_string(app::effective_host_port(port));
218261
}
@@ -544,6 +587,12 @@ namespace {
544587
"Cycle through the preferred video bitrate. Lower bitrates reduce bandwidth use and can help when running Sunshine and xemu on the same NATed host.",
545588
true,
546589
},
590+
{
591+
"cycle-video-decoder",
592+
std::string("Video Decoder: ") + video_decoder_selection_label(state.settings.videoDecoder),
593+
"Choose automatic codec negotiation or force one FFmpeg video decoder for new streams.",
594+
true,
595+
},
547596
{
548597
"toggle-play-audio-on-pc",
549598
std::string("Play Audio on PC: ") + (state.settings.playAudioOnPc ? "On" : "Off"),
@@ -1405,6 +1454,7 @@ namespace app {
14051454
state.settings.xemuConsoleLoggingLevel = logging::LogLevel::none;
14061455
state.settings.streamFramerate = DEFAULT_STREAM_FRAMERATE;
14071456
state.settings.streamBitrateKbps = DEFAULT_STREAM_BITRATE_KBPS;
1457+
state.settings.videoDecoder = VideoDecoderSelection::autoDetect;
14081458
state.settings.playAudioOnPc = false;
14091459
state.settings.showPerformanceStats = false;
14101460
state.settings.playAudioOnXbox = true;
@@ -1996,6 +2046,14 @@ namespace app {
19962046
rebuild_menu(state, "cycle-stream-bitrate");
19972047
return;
19982048
}
2049+
if (detailUpdate.activatedItemId == "cycle-video-decoder") {
2050+
cycle_video_decoder(state);
2051+
state.settings.dirty = true;
2052+
update->persistence.settingsChanged = true;
2053+
state.shell.statusMessage = std::string("Video decoder set to ") + video_decoder_selection_label(state.settings.videoDecoder);
2054+
rebuild_menu(state, "cycle-video-decoder");
2055+
return;
2056+
}
19992057
if (detailUpdate.activatedItemId == "toggle-play-audio-on-pc") {
20002058
state.settings.playAudioOnPc = !state.settings.playAudioOnPc;
20012059
state.settings.dirty = true;

src/app/client_state.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ namespace app {
6565
right, ///< Dock the log viewer on the right side of the split layout.
6666
};
6767

68+
/**
69+
* @brief Video decoder preference applied to new streaming sessions.
70+
*/
71+
enum class VideoDecoderSelection {
72+
autoDetect, ///< Let moonlight-common-c negotiate the best mutually supported video format.
73+
h264, ///< Force H.264 video decode.
74+
mpeg2, ///< Force MPEG-2/H.262 video decode.
75+
h263p, ///< Force H.263+ video decode.
76+
};
77+
6878
/**
6979
* @brief Focus areas used by the two-pane settings screen.
7080
*/
@@ -321,6 +331,7 @@ namespace app {
321331
bool preferredVideoModeSet = false; ///< True when preferredVideoMode contains a user-selected or default mode.
322332
int streamFramerate = 30; ///< Preferred stream frame rate in frames per second.
323333
int streamBitrateKbps = 1000; ///< Preferred stream bitrate in kilobits per second.
334+
VideoDecoderSelection videoDecoder = VideoDecoderSelection::autoDetect; ///< Preferred video decoder for new streams.
324335
bool playAudioOnPc = false; ///< True when the host PC should continue local audio playback during streaming.
325336
bool showPerformanceStats = false; ///< True when stream telemetry should be shown after streaming ends.
326337
bool playAudioOnXbox = true; ///< True when the Xbox should decode and play streamed audio locally.

src/app/settings_storage.cpp

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,27 @@ namespace {
127127
return "full";
128128
}
129129

130+
/**
131+
* @brief Convert a decoder preference to the persisted TOML token.
132+
*
133+
* @param selection Decoder preference to serialize.
134+
* @return Stable lowercase settings value.
135+
*/
136+
const char *video_decoder_selection_text(app::VideoDecoderSelection selection) {
137+
switch (selection) {
138+
case app::VideoDecoderSelection::autoDetect:
139+
return "auto";
140+
case app::VideoDecoderSelection::h264:
141+
return "h264";
142+
case app::VideoDecoderSelection::mpeg2:
143+
return "mpeg2";
144+
case app::VideoDecoderSelection::h263p:
145+
return "h263p";
146+
}
147+
148+
return "auto";
149+
}
150+
130151
bool try_parse_logging_level(std::string_view text, logging::LogLevel *level) {
131152
const std::string normalized = ascii_lowercase(text);
132153
if (normalized == "trace") {
@@ -193,6 +214,43 @@ namespace {
193214
return false;
194215
}
195216

217+
/**
218+
* @brief Parse a decoder preference from the persisted TOML token.
219+
*
220+
* @param text Settings value to parse.
221+
* @param selection Receives the parsed decoder preference on success.
222+
* @return True when @p text is a supported decoder preference.
223+
*/
224+
bool try_parse_video_decoder_selection(std::string_view text, app::VideoDecoderSelection *selection) {
225+
const std::string normalized = ascii_lowercase(text);
226+
if (normalized == "auto") {
227+
if (selection != nullptr) {
228+
*selection = app::VideoDecoderSelection::autoDetect;
229+
}
230+
return true;
231+
}
232+
if (normalized == "h264" || normalized == "h.264") {
233+
if (selection != nullptr) {
234+
*selection = app::VideoDecoderSelection::h264;
235+
}
236+
return true;
237+
}
238+
if (normalized == "mpeg2" || normalized == "mpeg-2" || normalized == "h262" || normalized == "h.262") {
239+
if (selection != nullptr) {
240+
*selection = app::VideoDecoderSelection::mpeg2;
241+
}
242+
return true;
243+
}
244+
if (normalized == "h263p" || normalized == "h.263p" || normalized == "h263+" || normalized == "h.263+") {
245+
if (selection != nullptr) {
246+
*selection = app::VideoDecoderSelection::h263p;
247+
}
248+
return true;
249+
}
250+
251+
return false;
252+
}
253+
196254
void append_invalid_value_warning(std::vector<std::string> *warnings, const std::string &filePath, std::string_view keyPath, std::string_view valueText) {
197255
if (warnings == nullptr) {
198256
return;
@@ -263,6 +321,34 @@ namespace {
263321
append_invalid_value_warning(warnings, filePath, "ui.log_viewer_placement", "<non-string>");
264322
}
265323

324+
/**
325+
* @brief Load one decoder preference setting when present.
326+
*
327+
* @param settingNode TOML node to parse.
328+
* @param filePath Settings file path used in warnings.
329+
* @param selection Receives the parsed decoder preference on success.
330+
* @param warnings Warning collection updated for invalid values.
331+
*/
332+
void load_video_decoder_selection_setting(
333+
toml::node_view<const toml::node> settingNode,
334+
const std::string &filePath,
335+
app::VideoDecoderSelection *selection,
336+
std::vector<std::string> *warnings
337+
) {
338+
if (!settingNode) {
339+
return;
340+
}
341+
342+
if (const auto videoDecoderText = settingNode.value<std::string>(); videoDecoderText) {
343+
if (!try_parse_video_decoder_selection(*videoDecoderText, selection)) {
344+
append_invalid_value_warning(warnings, filePath, "streaming.video_decoder", *videoDecoderText);
345+
}
346+
return;
347+
}
348+
349+
append_invalid_value_warning(warnings, filePath, "streaming.video_decoder", "<non-string>");
350+
}
351+
266352
/**
267353
* @brief Load one integer settings value when present.
268354
*
@@ -346,6 +432,8 @@ namespace {
346432
content += "# Preferred streaming parameters.\n";
347433
content += std::string("fps = ") + std::to_string(settings.streamFramerate) + "\n";
348434
content += std::string("bitrate_kbps = ") + std::to_string(settings.streamBitrateKbps) + "\n";
435+
content += "# Video decoder preference. Use auto to keep normal host negotiation.\n";
436+
content += std::string("video_decoder = \"") + video_decoder_selection_text(settings.videoDecoder) + "\"\n";
349437
content += std::string("play_audio_on_pc = ") + (settings.playAudioOnPc ? "true" : "false") + "\n";
350438
content += std::string("play_audio_on_xbox = ") + (settings.playAudioOnXbox ? "true" : "false") + "\n";
351439
content += "# Show stream telemetry after streaming ends.\n";
@@ -392,7 +480,7 @@ namespace {
392480
void inspect_streaming_keys(const toml::table &streamingTable, const std::string &filePath, app::LoadAppSettingsResult *result) {
393481
for (const auto &[rawKey, node] : streamingTable) {
394482
const std::string key(rawKey.str());
395-
if (key == "video_width" || key == "video_height" || key == "video_bpp" || key == "video_refresh" || key == "video_mode_selected" || key == "fps" || key == "bitrate_kbps" || key == "play_audio_on_pc" || key == "play_audio_on_xbox" || key == "show_performance_stats") {
483+
if (key == "video_width" || key == "video_height" || key == "video_bpp" || key == "video_refresh" || key == "video_mode_selected" || key == "fps" || key == "bitrate_kbps" || key == "video_decoder" || key == "play_audio_on_pc" || key == "play_audio_on_xbox" || key == "show_performance_stats") {
396484
continue;
397485
}
398486

@@ -490,6 +578,7 @@ namespace app {
490578
load_boolean_setting(settingsTable["streaming"]["video_mode_selected"], filePath, "streaming.video_mode_selected", &result.settings.preferredVideoModeSet, &result.warnings);
491579
load_integer_setting(settingsTable["streaming"]["fps"], filePath, "streaming.fps", &result.settings.streamFramerate, &result.warnings);
492580
load_integer_setting(settingsTable["streaming"]["bitrate_kbps"], filePath, "streaming.bitrate_kbps", &result.settings.streamBitrateKbps, &result.warnings);
581+
load_video_decoder_selection_setting(settingsTable["streaming"]["video_decoder"], filePath, &result.settings.videoDecoder, &result.warnings);
493582
load_boolean_setting(settingsTable["streaming"]["play_audio_on_pc"], filePath, "streaming.play_audio_on_pc", &result.settings.playAudioOnPc, &result.warnings);
494583
load_boolean_setting(settingsTable["streaming"]["play_audio_on_xbox"], filePath, "streaming.play_audio_on_xbox", &result.settings.playAudioOnXbox, &result.warnings);
495584
load_boolean_setting(settingsTable["streaming"]["show_performance_stats"], filePath, "streaming.show_performance_stats", &result.settings.showPerformanceStats, &result.warnings);

src/app/settings_storage.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace app {
2424
bool preferredVideoModeSet = false; ///< True when preferredVideoMode contains a saved user preference.
2525
int streamFramerate = 30; ///< Preferred stream frame rate in frames per second.
2626
int streamBitrateKbps = 1000; ///< Preferred stream bitrate in kilobits per second.
27+
app::VideoDecoderSelection videoDecoder = app::VideoDecoderSelection::autoDetect; ///< Preferred video decoder for new streams.
2728
bool playAudioOnPc = false; ///< True when the host PC should continue local audio playback during streaming.
2829
bool showPerformanceStats = false; ///< True when stream telemetry should be shown after streaming ends.
2930
bool playAudioOnXbox = true; ///< True when the Xbox should decode and play streamed audio locally.

src/main.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ namespace {
4444
state.settings.preferredVideoModeSet = settings.preferredVideoModeSet;
4545
state.settings.streamFramerate = settings.streamFramerate;
4646
state.settings.streamBitrateKbps = settings.streamBitrateKbps;
47+
state.settings.videoDecoder = settings.videoDecoder;
4748
state.settings.playAudioOnPc = settings.playAudioOnPc;
4849
state.settings.showPerformanceStats = settings.showPerformanceStats;
4950
state.settings.playAudioOnXbox = settings.playAudioOnXbox;

src/startup/video_mode.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,13 @@ namespace startup {
3939
return left.width == right.width && left.height == right.height;
4040
}
4141

42+
int stream_video_mode_area(const VIDEO_MODE &videoMode) {
43+
return videoMode.width * videoMode.height;
44+
}
45+
4246
bool is_smaller_video_mode(const VIDEO_MODE &left, const VIDEO_MODE &right) {
43-
const int leftArea = left.width * left.height;
44-
if (const int rightArea = right.width * right.height; leftArea != rightArea) {
47+
const int leftArea = stream_video_mode_area(left);
48+
if (const int rightArea = stream_video_mode_area(right); leftArea != rightArea) {
4549
return leftArea < rightArea;
4650
}
4751
return left.width < right.width;

0 commit comments

Comments
 (0)