mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
feat(neshub): act on the receiver's reports, and say what the bitrate is made of
Wires the controller to the connection. Reports arrive on the bidi stream the client already holds, one a second; the worst of them across attached clients drives the decision, because one encoder serves them all and the client that cannot decode is the one that matters -- averaging its trouble away leaves it never recovering while the numbers look fine. Reports are *taken* rather than read, so a client that stops reporting stops looking healthy. A report describes the second that just passed, and acting on it again the next second is acting on evidence that has expired. The fallback path view comes from the selected path rather than the connection as a whole: a connection typically holds one route through a relay and one direct, and only the selected one says anything about where the media is going. A client setting the bitrate by hand stands the controller down, and setting a mode brings it back. Overriding a person's setting a second later would remove the only tool that finds this class of bug -- it is how the original report was shown to be wrong about the bitrate having been lowered. The controller's own commands carry no codec and no bit depth, which matters more than it reads: the far end rebuilds its video session for anything that might be either, and a rebuild costs a keyframe. Saying nothing it does not mean is what keeps a per-second adjustment free. And the counter is split. One video byte count could not distinguish an encoder ignoring its target from a stream that is mostly keyframes, and those have opposite fixes -- a session overshooting tenfold looked identical either way, which is why the cause stayed ambiguous for weeks. Keyframe bits, delta bits and keyframes per second now travel separately, alongside what the controller is asking for, the ceiling it is working within, and why. Appended to the stats packet, so an older reader keeps working on the part it understands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,21 @@ pub fn encode_encode_settings(
|
||||
buf.push(bit_depth);
|
||||
}
|
||||
|
||||
/// A settings payload that changes the bitrate and nothing else.
|
||||
///
|
||||
/// Six bytes rather than seven: the bit-depth byte is *omitted*, which
|
||||
/// [`decode_encode_settings`] reports as `None`. That matters at the far end,
|
||||
/// where a change carrying a depth has to be treated as a possible depth change
|
||||
/// and rebuild the video session -- and a rebuild costs a keyframe. A controller
|
||||
/// adjusting the bitrate every second must not do that, so it says nothing it
|
||||
/// does not mean: keep the codec, keep the depth, this bitrate.
|
||||
pub fn encode_bitrate_only(buf: &mut Vec<u8>, kbps: u32) {
|
||||
buf.reserve(6);
|
||||
buf.push(CODEC_KEEP);
|
||||
buf.push(RC_CBR);
|
||||
buf.extend_from_slice(&kbps.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Decode an encode-settings payload. Returns `(codec_id, rate_control_mode, value, bit_depth)`.
|
||||
/// bit_depth is None for 6-byte (old client) payloads, Some(n) for 7+ byte payloads.
|
||||
pub fn decode_encode_settings(payload: &[u8]) -> Option<(u8, u8, u32, Option<u8>)> {
|
||||
@@ -457,6 +472,20 @@ mod media_control_tests {
|
||||
assert_eq!(decode_control_mode(&buf), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bitrate_only_change_carries_no_depth_and_no_codec() {
|
||||
// The far end rebuilds its video session -- and spends a keyframe -- for
|
||||
// anything that might be a codec or depth change. A controller nudging
|
||||
// the bitrate every second must say neither.
|
||||
let mut buf = Vec::new();
|
||||
encode_bitrate_only(&mut buf, 2_500);
|
||||
let (codec, rc, value, depth) = decode_encode_settings(&buf).expect("readable");
|
||||
assert_eq!(codec, CODEC_KEEP);
|
||||
assert_eq!(rc, RC_CBR);
|
||||
assert_eq!(value, 2_500);
|
||||
assert_eq!(depth, None, "a depth byte would force a rebuild");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_new_message_types_do_not_collide() {
|
||||
let all = [
|
||||
|
||||
@@ -60,6 +60,49 @@ pub fn encode_hub_stats(
|
||||
buf.push(audio_channels);
|
||||
}
|
||||
|
||||
/// What the video bitrate is made of, and who chose it.
|
||||
///
|
||||
/// Appended to a hub stats packet rather than replacing anything, so an older
|
||||
/// reader keeps working on the part it understands -- `decode_stats` already
|
||||
/// guards each field on the length it needs.
|
||||
///
|
||||
/// **The split is the point.** One combined byte counter cannot distinguish an
|
||||
/// encoder ignoring its bitrate target from a stream that is mostly keyframes,
|
||||
/// and those have opposite fixes. A session overshooting its target by ten times
|
||||
/// looked identical either way, which is why the cause stayed ambiguous for
|
||||
/// weeks.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct VideoBreakdown {
|
||||
/// Keyframe bits per second, measured over the last second.
|
||||
pub key_bps: u32,
|
||||
/// Everything else, measured the same way.
|
||||
pub delta_bps: u32,
|
||||
/// Keyframes in the last second.
|
||||
pub keyframes: u8,
|
||||
/// What the controller is asking the encoder for.
|
||||
pub target_kbps: u32,
|
||||
/// The ceiling it is choosing within.
|
||||
pub ceiling_kbps: u32,
|
||||
/// Why the target is what it is; see the hub's control module.
|
||||
pub reason: u8,
|
||||
/// 0 when the controller is deciding, 1 when a person set it by hand.
|
||||
pub manual: u8,
|
||||
}
|
||||
|
||||
/// `[4B key_bps][4B delta_bps][1B keyframes][4B target][4B ceiling][1B reason][1B manual]`
|
||||
pub const VIDEO_BREAKDOWN_LEN: usize = 19;
|
||||
|
||||
pub fn encode_video_breakdown(buf: &mut Vec<u8>, b: &VideoBreakdown) {
|
||||
buf.reserve(VIDEO_BREAKDOWN_LEN);
|
||||
buf.extend_from_slice(&b.key_bps.to_le_bytes());
|
||||
buf.extend_from_slice(&b.delta_bps.to_le_bytes());
|
||||
buf.push(b.keyframes);
|
||||
buf.extend_from_slice(&b.target_kbps.to_le_bytes());
|
||||
buf.extend_from_slice(&b.ceiling_kbps.to_le_bytes());
|
||||
buf.push(b.reason);
|
||||
buf.push(b.manual);
|
||||
}
|
||||
|
||||
/// Decoded stats from any source.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PipelineStats {
|
||||
@@ -76,6 +119,8 @@ pub struct PipelineStats {
|
||||
pub capture_attempts: u32,
|
||||
pub audio_bitrate_kbps: u32,
|
||||
pub audio_channels: u8,
|
||||
/// `None` from a hub that predates the breakdown.
|
||||
pub video: Option<VideoBreakdown>,
|
||||
}
|
||||
|
||||
/// Try to decode a single stats packet. The `msg_type` is the frame-level
|
||||
@@ -110,7 +155,106 @@ pub fn decode_stats(msg_type: u8, data: &[u8], stats: &mut PipelineStats) {
|
||||
if data.len() >= 14 {
|
||||
stats.audio_channels = data[13];
|
||||
}
|
||||
if data.len() >= 14 + VIDEO_BREAKDOWN_LEN {
|
||||
let d = &data[14..];
|
||||
let u32_at = |o: usize| u32::from_le_bytes(d[o..o + 4].try_into().unwrap());
|
||||
stats.video = Some(VideoBreakdown {
|
||||
key_bps: u32_at(0),
|
||||
delta_bps: u32_at(4),
|
||||
keyframes: d[8],
|
||||
target_kbps: u32_at(9),
|
||||
ceiling_kbps: u32_at(13),
|
||||
reason: d[17],
|
||||
manual: d[18],
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod breakdown_tests {
|
||||
use super::*;
|
||||
|
||||
fn breakdown() -> VideoBreakdown {
|
||||
VideoBreakdown {
|
||||
key_bps: 3_200_000,
|
||||
delta_bps: 6_800_000,
|
||||
keyframes: 2,
|
||||
target_kbps: 6_000,
|
||||
ceiling_kbps: 8_000,
|
||||
reason: 1,
|
||||
manual: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn hub_packet(with_breakdown: bool) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
encode_hub_stats(&mut buf, 1, 10_000_000, 0.0, 128, 2);
|
||||
if with_breakdown {
|
||||
encode_video_breakdown(&mut buf, &breakdown());
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_breakdown_survives_the_wire() {
|
||||
let packet = hub_packet(true);
|
||||
let mut stats = PipelineStats::default();
|
||||
decode_stats(STATS_HUB, &packet[1..], &mut stats);
|
||||
assert_eq!(stats.video, Some(breakdown()));
|
||||
// The fields that were always there still read correctly beside it.
|
||||
assert_eq!(stats.hub_clients, 1);
|
||||
assert_eq!(stats.audio_bitrate_kbps, 128);
|
||||
assert_eq!(stats.audio_channels, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hub_without_the_breakdown_still_reads() {
|
||||
// The reason this is appended rather than folded into the layout: a hub
|
||||
// that predates it keeps working, and says so by omission rather than by
|
||||
// reporting zeroes that look like a stream carrying nothing.
|
||||
let packet = hub_packet(false);
|
||||
let mut stats = PipelineStats::default();
|
||||
decode_stats(STATS_HUB, &packet[1..], &mut stats);
|
||||
assert_eq!(stats.video, None);
|
||||
assert_eq!(stats.hub_clients, 1);
|
||||
assert_eq!(stats.audio_channels, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_truncated_breakdown_is_left_out_rather_than_half_read() {
|
||||
let full = hub_packet(true);
|
||||
for n in 15..full.len() - 1 {
|
||||
let mut stats = PipelineStats::default();
|
||||
decode_stats(STATS_HUB, &full[1..n], &mut stats);
|
||||
assert_eq!(stats.video, None, "{n} bytes produced a partial breakdown");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_split_distinguishes_the_two_ways_a_stream_overshoots() {
|
||||
// The whole reason for the split. Same total, opposite causes: an
|
||||
// encoder ignoring its target, and a stream that is nearly all
|
||||
// keyframes. One counter cannot tell them apart.
|
||||
let ignoring_target = VideoBreakdown {
|
||||
key_bps: 200_000,
|
||||
delta_bps: 9_800_000,
|
||||
keyframes: 1,
|
||||
..breakdown()
|
||||
};
|
||||
let keyframe_storm = VideoBreakdown {
|
||||
key_bps: 9_000_000,
|
||||
delta_bps: 1_000_000,
|
||||
keyframes: 30,
|
||||
..breakdown()
|
||||
};
|
||||
assert_eq!(
|
||||
ignoring_target.key_bps + ignoring_target.delta_bps,
|
||||
keyframe_storm.key_bps + keyframe_storm.delta_bps,
|
||||
);
|
||||
assert!(ignoring_target.delta_bps > ignoring_target.key_bps);
|
||||
assert!(keyframe_storm.key_bps > keyframe_storm.delta_bps);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user