Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion moonshine-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ notify-rust = "4.17.0"
open = "5.3.5"
opus = "0.3.1"
pkcs8 = "0.11.0"
pixelforge = { git = "https://github.com/hgaiser/pixelforge", tag = "v0.7.2", features = ["dmabuf"] }
pixelforge = { git = "https://github.com/hgaiser/pixelforge", tag = "v0.8.0", features = ["dmabuf"] }
pulseaudio = "0.3.1"
rcgen = "0.14.8"
rsa = "0.9.10"
Expand Down
27 changes: 27 additions & 0 deletions moonshine-core/src/rtsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,32 @@ impl RtspServer {
let max_reference_frames: u32 =
get_optional_sdp_attribute(&sdp_session, "x-nv-video[0].maxNumReferenceFrames").unwrap_or(1);

// Bit 0 selects full-range luma, the remaining bits an SDR colorspace.
// Sunshine: `colorspace_from_client_config()`
const CSC_COLORSPACE_REC601: u32 = 0;
const CSC_COLORSPACE_REC709: u32 = 1;
let encoder_csc_mode: Option<u32> = get_optional_sdp_attribute(&sdp_session, "x-nv-video[0].encoderCscMode");
let full_range = encoder_csc_mode.unwrap_or_default() & 0x1 != 0;

// Only Rec.709 is encoded. Rec.601 doubles as the protocol default for
// clients that never chose a colorspace, so it stays quiet; higher
// values are explicit requests worth a warning.
match encoder_csc_mode.map(|mode| mode >> 1) {
Some(CSC_COLORSPACE_REC601) => {
tracing::debug!("Client sent SDR colorspace Rec.601 (the protocol default), encoding Rec.709.");
},
Some(colorspace) if colorspace != CSC_COLORSPACE_REC709 => {
tracing::warn!(
"Client requested SDR colorspace {colorspace} via encoderCscMode, encoding Rec.709 instead."
);
},
_ => {},
}
tracing::info!(
"Client requested {} range video.",
if full_range { "full" } else { "limited" }
);

// Parse the client's encryption flags from the ANNOUNCE SDP.
let client_encryption_flags: u8 =
get_optional_sdp_attribute(&sdp_session, "x-ss-general.encryptionEnabled").unwrap_or(0);
Expand All @@ -376,6 +402,7 @@ impl RtspServer {
dynamic_range,
chroma_sampling_type,
max_reference_frames,
full_range,
encrypt_video: self.video_config.encrypt && (client_encryption_flags & EncryptionFlags::Video as u8 != 0),
};

Expand Down
4 changes: 4 additions & 0 deletions moonshine-core/src/session/stream/video/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ pub struct VideoStreamContext {
/// Maximum number of reference frames for the video encoder.
pub max_reference_frames: u32,

/// Whether the client asked for full-range (0-255) rather than
/// limited-range (16-235) luma.
pub full_range: bool,

/// Whether the client has enabled video encryption.
pub encrypt_video: bool,
}
Expand Down
36 changes: 23 additions & 13 deletions moonshine-core/src/session/stream/video/pipeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,8 @@ impl VideoPipelineInner {

// Select color description for VUI signaling.
let color_description = match ctx.dynamic_range {
VideoDynamicRange::Sdr => ColorDescription::bt709(),
VideoDynamicRange::Hdr => ColorDescription::bt2020_pq(),
VideoDynamicRange::Sdr => ColorDescription::bt709().with_full_range(ctx.full_range),
VideoDynamicRange::Hdr => ColorDescription::bt2020_pq().with_full_range(ctx.full_range),
};

// Create encode configuration.
Expand Down Expand Up @@ -689,8 +689,8 @@ impl VideoPipelineInner {
// color_desc differs, we call set_color_description() to update
// the SPS/sequence header.
let mut encoder_color_desc: Option<ColorDescription> = Some(match ctx.dynamic_range {
VideoDynamicRange::Sdr => ColorDescription::bt709(),
VideoDynamicRange::Hdr => ColorDescription::bt2020_pq(),
VideoDynamicRange::Sdr => ColorDescription::bt709().with_full_range(ctx.full_range),
VideoDynamicRange::Hdr => ColorDescription::bt2020_pq().with_full_range(ctx.full_range),
});

while !stop_session_manager.is_shutdown_triggered() {
Expand Down Expand Up @@ -780,6 +780,11 @@ impl VideoPipelineInner {
match encoder.encode(encoder.input_image()) {
Ok(future) => {
let submitted_at = std::time::Instant::now();
// Deliberately still a whole-struct comparison, unlike the
// control-stream check above. Correcting it newly enables
// AV1 metadata injection for full-range clients, and that
// produces streams Moonlight cannot decode. Left until the
// AV1 path is fixed.
let inject_hdr = encoder_color_desc == Some(ColorDescription::bt2020_pq());
let frame_context = FrameContext {
created_at: now,
Expand Down Expand Up @@ -919,8 +924,8 @@ impl VideoPipelineInner {
Some(conv) => conv,
None => {
let (color_space, full_range) = match ctx.dynamic_range {
VideoDynamicRange::Sdr => (ColorSpace::Bt709, false),
VideoDynamicRange::Hdr => (ColorSpace::Bt2020, false),
VideoDynamicRange::Sdr => (ColorSpace::Bt709, ctx.full_range),
VideoDynamicRange::Hdr => (ColorSpace::Bt2020, ctx.full_range),
};
let mut config =
ColorConverterConfig::new(ctx.width, ctx.height, frame_input_format, output_format);
Expand Down Expand Up @@ -951,20 +956,20 @@ impl VideoPipelineInner {
let (cs, full_range, color_desc, sdr_white_nits) = match frame_cs {
FrameColorSpace::Srgb => (
ColorSpace::Bt709,
false,
ColorDescription::bt709(),
ctx.full_range,
ColorDescription::bt709().with_full_range(ctx.full_range),
BT2408_SDR_REFERENCE_NITS,
),
FrameColorSpace::Bt2020Pq => (
ColorSpace::Bt2020,
false,
ColorDescription::bt2020_pq(),
ctx.full_range,
ColorDescription::bt2020_pq().with_full_range(ctx.full_range),
BT2408_SDR_REFERENCE_NITS,
),
FrameColorSpace::ScrgbLinear => (
ColorSpace::Bt709LinearToBt2020Pq,
false,
ColorDescription::bt2020_pq(),
ctx.full_range,
ColorDescription::bt2020_pq().with_full_range(ctx.full_range),
SCRGB_REFERENCE_WHITE_NITS,
),
};
Expand Down Expand Up @@ -1010,7 +1015,7 @@ impl VideoPipelineInner {
// In HDR sessions, `enabled` reflects whether the current
// frame is encoded as BT.2020+PQ (true) or BT.709 (false).
if ctx.dynamic_range == VideoDynamicRange::Hdr {
let hdr_enabled = encoder_color_desc == Some(ColorDescription::bt2020_pq());
let hdr_enabled = encoder_color_desc.is_some_and(|desc| desc.is_hdr());
let new_state = HdrModeState {
enabled: hdr_enabled,
metadata: frame.hdr_metadata,
Expand Down Expand Up @@ -1044,6 +1049,11 @@ impl VideoPipelineInner {
// Hand this frame's context plus its packet future to the
// consumer thread, which awaits the future, injects HDR SEI if
// needed, packetizes and sends it, and records stats.
// Deliberately still a whole-struct comparison, unlike the
// control-stream check above. Correcting it newly enables
// AV1 metadata injection for full-range clients, and that
// produces streams Moonlight cannot decode. Left until the
// AV1 path is fixed.
let inject_hdr = encoder_color_desc == Some(ColorDescription::bt2020_pq());
let frame_context = FrameContext {
created_at: frame.created_at,
Expand Down
1 change: 1 addition & 0 deletions moonshine-tools/src/bin/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ async fn run_benchmark(
},
chroma_sampling_type: VideoChromaSampling::Yuv420,
max_reference_frames: 1,
full_range: false,
encrypt_video: false,
};

Expand Down