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
11 changes: 8 additions & 3 deletions docs/qt-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,15 @@ leave the original account active and show an error on the account page. Test bo
and console modes; synthetic account fixtures do not prove a provider accepts login.

Video SETUP tries a bounded set of control-URI and Transport forms within one request
budget. A successful response must still supply a usable server-authored video endpoint.
After all forms fail to supply one, `missing-video-peer` is a terminal negotiation error,
budget. When the server answers 200 without a usable video endpoint (a rig whose
video streamer is still starting), the sweep repeats on a bounded pace (3s pauses,
at most 3 extra rounds inside the same budget) instead of failing in under a
second. A successful response must still supply a usable server-authored video
endpoint. After all rounds fail to supply one, `missing-video-peer` is a terminal
negotiation error,
not a reason to repeatedly reclaim the same seat. Unsupported legacy transport is also
terminal. Transient network failures retain the existing bounded session recovery.
terminal. Pure rejections (400/404/459+) still fail fast without retries, and transient
network failures retain the existing bounded session recovery.

For a partner that still cannot start, reproduce once and export diagnostics. Keep the
`video-setup` and `video-setup-transport` lines. They describe response status, field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ mod transport_diagnostics;
use color::NvstColorNegotiation;

const REQUEST_TIMEOUT: Duration = Duration::from_secs(20);
// A rig whose video streamer is still starting answers SETUP with 200 but no
// Transport peer yet. Re-sweep on a bounded pace then instead of failing in
// under a second; pure rejections still fail immediately. Worst case adds 9s
// inside the shared 20s budget above.
const SETUP_PEER_RETRY_ROUNDS: u32 = 3;
const SETUP_PEER_RETRY_DELAY: Duration = Duration::from_secs(3);
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(2);
#[cfg(test)]
const CONTROL_PING_EXPIRY: Duration = Duration::from_secs(5);
Expand Down Expand Up @@ -114,11 +120,77 @@ impl RtspClient {
headers: &[(&str, String)],
client_port: u16,
) -> Result<VideoSetup, NvstRtspError> {
self.setup_video_with_retry(
control,
target,
headers,
client_port,
SETUP_PEER_RETRY_ROUNDS,
SETUP_PEER_RETRY_DELAY,
)
}

fn setup_video_with_retry(
&mut self,
control: &str,
target: &str,
headers: &[(&str, String)],
client_port: u16,
max_peer_retries: u32,
peer_retry_delay: Duration,
) -> Result<VideoSetup, NvstRtspError> {
// A rig whose video streamer is still starting answers SETUP with 200
// but no Transport peer yet. Re-sweep on a bounded pace then: the
// official client negotiates through progress callbacks instead of
// one fast burst. Pure rejections (400/404/459+) mean the forms are
// wrong for this server, so those still fail immediately.
let candidates = video_setup_candidates(control, target);
let deadline = Instant::now() + REQUEST_TIMEOUT;
let mut headers = headers.to_vec();
headers.push(("Transport", String::new()));
let transport_index = headers.len() - 1;
let mut round = 0u32;
loop {
match self.setup_video_sweep(
&candidates,
&mut headers,
transport_index,
client_port,
&deadline,
) {
Ok(setup) => return Ok(setup),
Err(error) if error.code != "missing-video-peer" => return Err(error),
Err(error) if round >= max_peer_retries => return Err(error),
Err(error) => {
round += 1;
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(error);
}
let sleep_for = peer_retry_delay.min(remaining);
opennow_streamer_protocol::log::log_line(
"INFO",
"rtsps",
&format!(
"video-setup-retry round={round}/{max_peer_retries} sleep_ms={}",
sleep_for.as_millis(),
),
);
std::thread::sleep(sleep_for);
}
}
}
}

#[allow(clippy::too_many_arguments)]
fn setup_video_sweep(
&mut self,
candidates: &[String],
headers: &mut [(&str, String)],
transport_index: usize,
client_port: u16,
deadline: &Instant,
) -> Result<VideoSetup, NvstRtspError> {
let mut missing_peer = false;
let mut last_status = 0;
for transport in [
Expand All @@ -143,7 +215,7 @@ impl RtspClient {
));
}
let response =
self.request_with_timeout("SETUP", candidate, &headers, "", remaining)?;
self.request_with_timeout("SETUP", candidate, headers, "", remaining)?;
let transport = header_value(&response, "transport");
let peer = transport
.and_then(parse_video_peer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const EMPTY_TRANSPORT_URIS: [&str; 4] = [
"rtsps://seat.nvidiagrid.net:322/streamid=video/0",
];

#[derive(Clone, Copy)]
struct Reply {
uri: &'static str,
transport: &'static str,
Expand All @@ -18,6 +19,14 @@ struct Reply {
}

fn scripted_setup(replies: Vec<Reply>) -> Result<VideoSetup, NvstRtspError> {
scripted_setup_with_retry(replies, 0, Duration::ZERO)
}

fn scripted_setup_with_retry(
replies: Vec<Reply>,
max_peer_retries: u32,
peer_retry_delay: Duration,
) -> Result<VideoSetup, NvstRtspError> {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let stream = TcpStream::connect(address).unwrap();
Expand Down Expand Up @@ -67,7 +76,7 @@ fn scripted_setup(replies: Vec<Reply>) -> Result<VideoSetup, NvstRtspError> {
cseq: 2,
buffer: String::new(),
};
let result = client.setup_video(
let result = client.setup_video_with_retry(
"streamid=video/0",
TARGET,
&[
Expand All @@ -76,6 +85,8 @@ fn scripted_setup(replies: Vec<Reply>) -> Result<VideoSetup, NvstRtspError> {
("x-nv-ping", "6".to_owned()),
],
49005,
max_peer_retries,
peer_retry_delay,
);
drop(client);
server.join().unwrap();
Expand Down Expand Up @@ -305,3 +316,77 @@ fn rtsp_request_deadline_bounds_a_partial_response() {
"deadline exceeded: {elapsed:?}"
);
}

#[test]
fn video_setup_resweeps_when_successes_omit_a_peer_until_the_rig_is_ready() {
// Round 1: the rig 200s every form but has no video peer yet. Round 2:
// the first URI succeeds with a peer. Mirrors a late-starting encoder.
let mut replies: Vec<_> = ["", "unicast;X-GS-ClientPort=49005-49006"]
.iter()
.flat_map(|transport| {
EMPTY_TRANSPORT_URIS.iter().map(move |uri| Reply {
uri,
transport,
status: 200,
headers: "",
})
})
.collect();
replies.push(Reply {
uri: EMPTY_TRANSPORT_URIS[0],
transport: "",
status: 200,
headers: VALID_PEER,
});
let setup = scripted_setup_with_retry(replies, 3, Duration::ZERO).unwrap();
assert_eq!(setup.peer, ("192.0.2.10".to_owned(), 5004, 5005));
}

#[test]
fn video_setup_peer_retry_stays_bounded_and_keeps_the_terminal_code() {
// Every round 200s without a peer: retries exhaust, then the original
// missing-video-peer error (not a timeout, not a new code) is returned.
let one_round: Vec<_> = ["", "unicast;X-GS-ClientPort=49005-49006"]
.iter()
.flat_map(|transport| {
EMPTY_TRANSPORT_URIS.iter().map(move |uri| Reply {
uri,
transport,
status: 200,
headers: "",
})
})
.collect();
let mut replies = Vec::new();
for _ in 0..3 {
replies.extend(one_round.iter().cloned());
}
let error = match scripted_setup_with_retry(replies, 2, Duration::ZERO) {
Ok(_) => panic!("SETUP without a peer must not succeed"),
Err(error) => error,
};
assert_eq!(error.code, "missing-video-peer");
assert!(error.message.contains("4 URI forms and 2 Transport forms"));
}

#[test]
fn video_setup_never_retries_pure_rejections() {
// No 200 seen at all: the forms are wrong for this server, so fail fast
// without burning retry rounds.
let replies: Vec<_> = ["", "unicast;X-GS-ClientPort=49005-49006"]
.iter()
.flat_map(|transport| {
EMPTY_TRANSPORT_URIS.iter().map(move |uri| Reply {
uri,
transport,
status: 400,
headers: "",
})
})
.collect();
let error = match scripted_setup_with_retry(replies, 3, Duration::ZERO) {
Ok(_) => panic!("rejected SETUP forms must not succeed"),
Err(error) => error,
};
assert_eq!(error.code, "nvst-rtsp-failed");
}
Loading