Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
![Apps Screen](./docs/images/screenshots/03-apps.png)


## Video codec selection

The streaming menu's **Video Codec** setting offers Auto, H.264, MPEG-2, and H.263+.
Auto prefers H.264, then MPEG-2, then H.263+, using only codecs advertised by the host.
An explicit choice requests only that codec and fails negotiation when it is unavailable.
Sunshine must enable `mpeg2_mode = 2` or `h263p_mode = 2` to advertise the corresponding legacy codec.
These Sunshine protocol extensions are available to any client implementing their negotiation and decoding.

The preference is saved as `streaming.video_codec` in the settings file. Existing
`streaming.video_decoder` preferences are read for compatibility; `video_codec` takes precedence when both exist.

## Streaming controls and statistics

To quit a stream, press **Back/Select + Black + White (LB + RB)** together on any connected controller.
Expand Down
8 changes: 7 additions & 1 deletion cmake/moonlight-dependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function(_moonlight_compute_ffmpeg_signature out_var nxdk_dir ffmpeg_source_dir)
"NXDK_DIR=${nxdk_dir}"
"NXDK_REVISION=${nxdk_revision}"
"COMPAT_SHA256=${compat_hashes}"
"FFMPEG_PROFILE=h264-opus-xbox"
"FFMPEG_PROFILE=h264-mpeg2-h263-opus-xbox"
"FFMPEG_TARGET_OS=none"
"FFMPEG_ARCH=x86"
"FFMPEG_CC_WRAPPER_SHA256=${ffmpeg_cc_wrapper_hash}"
Expand Down Expand Up @@ -238,7 +238,13 @@ function(_moonlight_get_ffmpeg_configure_args out_var)
--enable-swscale
--enable-swresample
--enable-parser=h264
--enable-parser=h263
--enable-parser=mpegvideo
--enable-decoder=h264
--enable-decoder=h263
--enable-decoder=h263i
--enable-decoder=h263p
--enable-decoder=mpeg2video
--enable-decoder=opus)

set(${out_var} "${ffmpeg_configure_args}" PARENT_SCOPE)
Expand Down
58 changes: 58 additions & 0 deletions src/app/client_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ namespace {
constexpr int DEFAULT_STREAM_BITRATE_KBPS = 1000;
constexpr std::array<int, 6> STREAM_FRAMERATE_OPTIONS {15, 20, 24, 25, 30, 60};
constexpr std::array<int, 9> STREAM_BITRATE_OPTIONS {500, 750, 1000, 1500, 2000, 2500, 3000, 4000, 5000};
constexpr std::array<app::VideoCodecSelection, 4> VIDEO_CODEC_OPTIONS {
app::VideoCodecSelection::autoDetect,
app::VideoCodecSelection::h264,
app::VideoCodecSelection::mpeg2,
app::VideoCodecSelection::h263p,
};

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

/**
* @brief Return the settings label for one video codec preference.
*
* @param selection Codec preference to describe.
* @return User-facing codec preference label.
*/
const char *video_codec_selection_label(app::VideoCodecSelection selection) {
switch (selection) {
case app::VideoCodecSelection::autoDetect:
return "Auto";
case app::VideoCodecSelection::h264:
return "H.264";
case app::VideoCodecSelection::mpeg2:
return "MPEG-2/H.262";
case app::VideoCodecSelection::h263p:
return "H.263+";
}

return "Auto";
}

/**
* @brief Return the selected stream-resolution index inside the detected mode list.
*
Expand Down Expand Up @@ -213,6 +240,22 @@ namespace {
state.settings.streamBitrateKbps = STREAM_BITRATE_OPTIONS[nextIndex];
}

/**
* @brief Advance the preferred video codec to the next supported option.
*
* @param state Current client state containing the preferred decoder.
*/
void cycle_video_codec(app::ClientState &state) {
const auto current = std::find(VIDEO_CODEC_OPTIONS.begin(), VIDEO_CODEC_OPTIONS.end(), state.settings.videoCodec);
if (current == VIDEO_CODEC_OPTIONS.end()) {
state.settings.videoCodec = app::VideoCodecSelection::autoDetect;
return;
}

const std::size_t nextIndex = (static_cast<std::size_t>(std::distance(VIDEO_CODEC_OPTIONS.begin(), current)) + 1U) % VIDEO_CODEC_OPTIONS.size();
state.settings.videoCodec = VIDEO_CODEC_OPTIONS[nextIndex];
}

std::string pairing_reset_endpoint_key(std::string_view address, uint16_t port) {
return app::normalize_ipv4_address(address) + ":" + std::to_string(app::effective_host_port(port));
}
Expand Down Expand Up @@ -544,6 +587,12 @@ namespace {
"Cycle through the preferred video bitrate. Lower bitrates reduce bandwidth use and can help when running Sunshine and xemu on the same NATed host.",
true,
},
{
"cycle-video-codec",
std::string("Video Codec: ") + video_codec_selection_label(state.settings.videoCodec),
"Choose automatic codec negotiation or request a specific video codec for new streams.",
true,
},
{
"toggle-play-audio-on-pc",
std::string("Play Audio on PC: ") + (state.settings.playAudioOnPc ? "On" : "Off"),
Expand Down Expand Up @@ -1405,6 +1454,7 @@ namespace app {
state.settings.xemuConsoleLoggingLevel = logging::LogLevel::none;
state.settings.streamFramerate = DEFAULT_STREAM_FRAMERATE;
state.settings.streamBitrateKbps = DEFAULT_STREAM_BITRATE_KBPS;
state.settings.videoCodec = VideoCodecSelection::autoDetect;
state.settings.playAudioOnPc = false;
state.settings.showPerformanceStats = false;
state.settings.playAudioOnXbox = true;
Expand Down Expand Up @@ -1996,6 +2046,14 @@ namespace app {
rebuild_menu(state, "cycle-stream-bitrate");
return;
}
if (detailUpdate.activatedItemId == "cycle-video-codec") {
cycle_video_codec(state);
state.settings.dirty = true;
update->persistence.settingsChanged = true;
state.shell.statusMessage = std::string("Video codec set to ") + video_codec_selection_label(state.settings.videoCodec);
rebuild_menu(state, "cycle-video-codec");
return;
}
if (detailUpdate.activatedItemId == "toggle-play-audio-on-pc") {
state.settings.playAudioOnPc = !state.settings.playAudioOnPc;
state.settings.dirty = true;
Expand Down
11 changes: 11 additions & 0 deletions src/app/client_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ namespace app {
right, ///< Dock the log viewer on the right side of the split layout.
};

/**
* @brief Video codec preference applied to new streaming sessions.
*/
enum class VideoCodecSelection {
autoDetect, ///< Negotiate supported codecs using the shared modern-first preference order.
h264, ///< Request only H.264 video.
mpeg2, ///< Request only MPEG-2/H.262 video.
h263p, ///< Request only H.263+ video.
};

/**
* @brief Focus areas used by the two-pane settings screen.
*/
Expand Down Expand Up @@ -321,6 +331,7 @@ namespace app {
bool preferredVideoModeSet = false; ///< True when preferredVideoMode contains a user-selected or default mode.
int streamFramerate = 30; ///< Preferred stream frame rate in frames per second.
int streamBitrateKbps = 1000; ///< Preferred stream bitrate in kilobits per second.
VideoCodecSelection videoCodec = VideoCodecSelection::autoDetect; ///< Preferred video codec for new streams.
bool playAudioOnPc = false; ///< True when the host PC should continue local audio playback during streaming.
bool showPerformanceStats = false; ///< Display live performance telemetry and include it in the stream summary.
bool playAudioOnXbox = true; ///< True when the Xbox should decode and play streamed audio locally.
Expand Down
98 changes: 97 additions & 1 deletion src/app/settings_storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ namespace {
return "full";
}

/**
* @brief Convert a codec preference to the persisted TOML token.
*
* @param selection Codec preference to serialize.
* @return Stable lowercase settings value.
*/
const char *video_codec_selection_text(app::VideoCodecSelection selection) {
switch (selection) {
case app::VideoCodecSelection::autoDetect:
return "auto";
case app::VideoCodecSelection::h264:
return "h264";
case app::VideoCodecSelection::mpeg2:
return "mpeg2";
case app::VideoCodecSelection::h263p:
return "h263p";
}

return "auto";
}

bool try_parse_logging_level(std::string_view text, logging::LogLevel *level) {
const std::string normalized = ascii_lowercase(text);
if (normalized == "trace") {
Expand Down Expand Up @@ -193,6 +214,43 @@ namespace {
return false;
}

/**
* @brief Parse a codec preference from the persisted TOML token.
*
* @param text Settings value to parse.
* @param selection Receives the parsed codec preference on success.
* @return True when @p text is a supported codec preference.
*/
bool try_parse_video_codec_selection(std::string_view text, app::VideoCodecSelection *selection) {
const std::string normalized = ascii_lowercase(text);
if (normalized == "auto") {
if (selection != nullptr) {
*selection = app::VideoCodecSelection::autoDetect;
}
return true;
}
if (normalized == "h264" || normalized == "h.264") {
if (selection != nullptr) {
*selection = app::VideoCodecSelection::h264;
}
return true;
}
if (normalized == "mpeg2" || normalized == "mpeg-2" || normalized == "h262" || normalized == "h.262") {
if (selection != nullptr) {
*selection = app::VideoCodecSelection::mpeg2;
}
return true;
}
if (normalized == "h263p" || normalized == "h.263p" || normalized == "h263+" || normalized == "h.263+") {
if (selection != nullptr) {
*selection = app::VideoCodecSelection::h263p;
}
return true;
}

return false;
}

void append_invalid_value_warning(std::vector<std::string> *warnings, const std::string &filePath, std::string_view keyPath, std::string_view valueText) {
if (warnings == nullptr) {
return;
Expand Down Expand Up @@ -263,6 +321,36 @@ namespace {
append_invalid_value_warning(warnings, filePath, "ui.log_viewer_placement", "<non-string>");
}

/**
* @brief Load one codec preference setting when present.
*
* @param settingNode TOML node to parse.
* @param filePath Settings file path used in warnings.
* @param keyPath Canonical or legacy setting key reported in validation warnings.
* @param selection Receives the parsed codec preference on success.
* @param warnings Warning collection updated for invalid values.
*/
void load_video_codec_selection_setting(
toml::node_view<const toml::node> settingNode,
const std::string &filePath,
std::string_view keyPath,
app::VideoCodecSelection *selection,
std::vector<std::string> *warnings
) {
if (!settingNode) {
return;
}

if (const auto videoCodecText = settingNode.value<std::string>(); videoCodecText) {
if (!try_parse_video_codec_selection(*videoCodecText, selection)) {
append_invalid_value_warning(warnings, filePath, keyPath, *videoCodecText);
}
return;
}

append_invalid_value_warning(warnings, filePath, keyPath, "<non-string>");
}

/**
* @brief Load one integer settings value when present.
*
Expand Down Expand Up @@ -346,6 +434,8 @@ namespace {
content += "# Preferred streaming parameters.\n";
content += std::string("fps = ") + std::to_string(settings.streamFramerate) + "\n";
content += std::string("bitrate_kbps = ") + std::to_string(settings.streamBitrateKbps) + "\n";
content += "# Video codec preference. Use auto to keep normal host negotiation.\n";
content += std::string("video_codec = \"") + video_codec_selection_text(settings.videoCodec) + "\"\n";
content += std::string("play_audio_on_pc = ") + (settings.playAudioOnPc ? "true" : "false") + "\n";
content += std::string("play_audio_on_xbox = ") + (settings.playAudioOnXbox ? "true" : "false") + "\n";
content += "# Show stream telemetry after streaming ends.\n";
Expand Down Expand Up @@ -392,7 +482,7 @@ namespace {
void inspect_streaming_keys(const toml::table &streamingTable, const std::string &filePath, app::LoadAppSettingsResult *result) {
for (const auto &[rawKey, node] : streamingTable) {
const std::string key(rawKey.str());
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") {
if (key == "video_width" || key == "video_height" || key == "video_bpp" || key == "video_refresh" || key == "video_mode_selected" || key == "fps" || key == "bitrate_kbps" || key == "video_codec" || key == "video_decoder" || key == "play_audio_on_pc" || key == "play_audio_on_xbox" || key == "show_performance_stats") {
continue;
}

Expand Down Expand Up @@ -490,6 +580,12 @@ namespace app {
load_boolean_setting(settingsTable["streaming"]["video_mode_selected"], filePath, "streaming.video_mode_selected", &result.settings.preferredVideoModeSet, &result.warnings);
load_integer_setting(settingsTable["streaming"]["fps"], filePath, "streaming.fps", &result.settings.streamFramerate, &result.warnings);
load_integer_setting(settingsTable["streaming"]["bitrate_kbps"], filePath, "streaming.bitrate_kbps", &result.settings.streamBitrateKbps, &result.warnings);
const auto codecNode = settingsTable["streaming"]["video_codec"];
if (codecNode) {
load_video_codec_selection_setting(codecNode, filePath, "streaming.video_codec", &result.settings.videoCodec, &result.warnings);
} else {
load_video_codec_selection_setting(settingsTable["streaming"]["video_decoder"], filePath, "streaming.video_decoder", &result.settings.videoCodec, &result.warnings);
}
load_boolean_setting(settingsTable["streaming"]["play_audio_on_pc"], filePath, "streaming.play_audio_on_pc", &result.settings.playAudioOnPc, &result.warnings);
load_boolean_setting(settingsTable["streaming"]["play_audio_on_xbox"], filePath, "streaming.play_audio_on_xbox", &result.settings.playAudioOnXbox, &result.warnings);
load_boolean_setting(settingsTable["streaming"]["show_performance_stats"], filePath, "streaming.show_performance_stats", &result.settings.showPerformanceStats, &result.warnings);
Expand Down
1 change: 1 addition & 0 deletions src/app/settings_storage.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace app {
bool preferredVideoModeSet = false; ///< True when preferredVideoMode contains a saved user preference.
int streamFramerate = 30; ///< Preferred stream frame rate in frames per second.
int streamBitrateKbps = 1000; ///< Preferred stream bitrate in kilobits per second.
app::VideoCodecSelection videoCodec = app::VideoCodecSelection::autoDetect; ///< Preferred video codec for new streams.
bool playAudioOnPc = false; ///< True when the host PC should continue local audio playback during streaming.
bool showPerformanceStats = false; ///< Display live performance telemetry and include it in the stream summary.
bool playAudioOnXbox = true; ///< True when the Xbox should decode and play streamed audio locally.
Expand Down
1 change: 1 addition & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ namespace {
state.settings.preferredVideoModeSet = settings.preferredVideoModeSet;
state.settings.streamFramerate = settings.streamFramerate;
state.settings.streamBitrateKbps = settings.streamBitrateKbps;
state.settings.videoCodec = settings.videoCodec;
state.settings.playAudioOnPc = settings.playAudioOnPc;
state.settings.showPerformanceStats = settings.showPerformanceStats;
state.settings.playAudioOnXbox = settings.playAudioOnXbox;
Expand Down
8 changes: 6 additions & 2 deletions src/startup/video_mode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ namespace startup {
return left.width == right.width && left.height == right.height;
}

int stream_video_mode_area(const VIDEO_MODE &videoMode) {
return videoMode.width * videoMode.height;
}

bool is_smaller_video_mode(const VIDEO_MODE &left, const VIDEO_MODE &right) {
const int leftArea = left.width * left.height;
if (const int rightArea = right.width * right.height; leftArea != rightArea) {
const int leftArea = stream_video_mode_area(left);
if (const int rightArea = stream_video_mode_area(right); leftArea != rightArea) {
return leftArea < rightArea;
}
return left.width < right.width;
Expand Down
Loading
Loading