fix: 修复 rtsp 服务连接错误

This commit is contained in:
mofeng-git
2026-02-11 20:00:33 +08:00
parent 934dc48208
commit 3133db9c86
6 changed files with 287 additions and 88 deletions

View File

@@ -1,8 +1,10 @@
use bytes::Bytes;
use base64::Engine;
use bytes::Bytes;
use rand::Rng;
use rtp::packet::Packet;
use rtp::packetizer::Payloader;
use rtsp_types as rtsp;
use sdp_types as sdp;
use std::collections::HashMap;
use std::io;
use std::net::SocketAddr;
@@ -10,9 +12,8 @@ use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, Mutex, RwLock};
use tokio::time::{sleep, Duration};
use webrtc::util::Marshal;
use rtsp_types as rtsp;
use sdp_types as sdp;
use crate::config::{RtspCodec, RtspConfig};
use crate::error::{AppError, Result};
@@ -26,6 +27,7 @@ use crate::webrtc::rtp::parse_profile_level_id_from_sps;
const RTP_CLOCK_RATE: u32 = 90_000;
const RTP_MTU: usize = 1200;
const RTSP_BUF_SIZE: usize = 8192;
const RTSP_RESUBSCRIBE_DELAY_MS: u64 = 300;
#[derive(Debug, Clone, PartialEq)]
pub enum RtspServiceStatus {
@@ -150,9 +152,9 @@ impl RtspService {
.parse()
.map_err(|e| AppError::BadRequest(format!("Invalid RTSP bind address: {}", e)))?;
let listener = TcpListener::bind(bind_addr)
.await
.map_err(|e| AppError::Io(io::Error::new(e.kind(), format!("RTSP bind failed: {}", e))))?;
let listener = TcpListener::bind(bind_addr).await.map_err(|e| {
AppError::Io(io::Error::new(e.kind(), format!("RTSP bind failed: {}", e)))
})?;
let service_config = self.config.clone();
let video_manager = self.video_manager.clone();
@@ -245,8 +247,14 @@ async fn handle_client(
) -> Result<()> {
let cfg_snapshot = config.read().await.clone();
let auth_enabled = cfg_snapshot.username.as_ref().is_some_and(|u| !u.is_empty())
|| cfg_snapshot.password.as_ref().is_some_and(|p| !p.is_empty());
let auth_enabled = cfg_snapshot
.username
.as_ref()
.is_some_and(|u| !u.is_empty())
|| cfg_snapshot
.password
.as_ref()
.is_some_and(|p| !p.is_empty());
if cfg_snapshot.allow_one_client {
let mut active_guard = shared.active_client.lock().await;
@@ -288,17 +296,8 @@ async fn handle_client(
}
};
if !is_valid_rtsp_path(&req.uri, &cfg_snapshot.path) {
send_response(
&mut stream,
&req,
404,
"Not Found",
vec![],
"",
"",
)
.await?;
if !is_valid_rtsp_path(&req.method, &req.uri, &cfg_snapshot.path) {
send_response(&mut stream, &req, 404, "Not Found", vec![], "", "").await?;
continue;
}
@@ -368,21 +367,31 @@ async fn handle_client(
&req,
200,
"OK",
vec![(
"Content-Type".to_string(),
"application/sdp".to_string(),
)],
vec![("Content-Type".to_string(), "application/sdp".to_string())],
&sdp,
&state.session_id,
)
.await?;
}
rtsp::Method::Setup => {
let transport = req
.headers
.get("transport")
.cloned()
.unwrap_or_default();
let transport = req.headers.get("transport").cloned().unwrap_or_default();
if !is_tcp_transport_request(&transport) {
send_response(
&mut stream,
&req,
461,
"Unsupported Transport",
vec![(
"Transport".to_string(),
"RTP/AVP/TCP;unicast;interleaved=0-1".to_string(),
)],
"",
&state.session_id,
)
.await?;
continue;
}
let interleaved = parse_interleaved_channel(&transport).unwrap_or(0);
state.setup_done = true;
@@ -420,16 +429,8 @@ async fn handle_client(
continue;
}
send_response(
&mut stream,
&req,
200,
"OK",
vec![],
"",
&state.session_id,
)
.await?;
send_response(&mut stream, &req, 200, "OK", vec![], "", &state.session_id)
.await?;
if let Err(e) = stream_video_interleaved(
stream,
@@ -447,16 +448,8 @@ async fn handle_client(
break 'client_loop;
}
rtsp::Method::Teardown => {
send_response(
&mut stream,
&req,
200,
"OK",
vec![],
"",
&state.session_id,
)
.await?;
send_response(&mut stream, &req, 200, "OK", vec![], "", &state.session_id)
.await?;
break 'client_loop;
}
_ => {
@@ -498,7 +491,9 @@ async fn stream_video_interleaved(
let mut rx = video_manager
.subscribe_encoded_frames()
.await
.ok_or_else(|| AppError::VideoError("RTSP failed to subscribe encoded frames".to_string()))?;
.ok_or_else(|| {
AppError::VideoError("RTSP failed to subscribe encoded frames".to_string())
})?;
video_manager.request_keyframe().await.ok();
@@ -518,7 +513,21 @@ async fn stream_video_interleaved(
tokio::select! {
maybe_frame = rx.recv() => {
let Some(frame) = maybe_frame else {
break;
tracing::warn!("RTSP encoded frame subscription ended, attempting to restart pipeline");
if let Some(new_rx) = video_manager.subscribe_encoded_frames().await {
rx = new_rx;
let _ = video_manager.request_keyframe().await;
tracing::info!("RTSP frame subscription recovered");
} else {
tracing::warn!(
"RTSP failed to resubscribe encoded frames, retrying in {}ms",
RTSP_RESUBSCRIBE_DELAY_MS
);
sleep(Duration::from_millis(RTSP_RESUBSCRIBE_DELAY_MS)).await;
}
continue;
};
if !is_frame_codec_match(&frame, &rtsp_codec) {
@@ -689,11 +698,14 @@ fn take_rtsp_request_from_buffer(buffer: &mut Vec<u8>) -> Option<String> {
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|window| window == needle)
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn parse_rtsp_request(raw: &str) -> Option<RtspRequest> {
let (message, consumed): (rtsp::Message<Vec<u8>>, usize) = rtsp::Message::parse(raw.as_bytes()).ok()?;
let (message, consumed): (rtsp::Message<Vec<u8>>, usize) =
rtsp::Message::parse(raw.as_bytes()).ok()?;
if consumed != raw.len() {
return None;
}
@@ -745,6 +757,14 @@ fn parse_interleaved_channel(transport: &str) -> Option<u8> {
None
}
fn is_tcp_transport_request(transport: &str) -> bool {
transport
.split(',')
.map(str::trim)
.map(str::to_ascii_lowercase)
.any(|item| item.contains("rtp/avp/tcp") || item.contains("interleaved="))
}
fn update_parameter_sets(params: &mut ParameterSets, frame: &EncodedVideoFrame) {
let nal_units = split_annexb_nal_units(frame.data.as_ref());
@@ -1036,18 +1056,33 @@ fn status_code_from_u16(code: u16) -> rtsp::StatusCode {
405 => rtsp::StatusCode::MethodNotAllowed,
453 => rtsp::StatusCode::NotEnoughBandwidth,
455 => rtsp::StatusCode::MethodNotValidInThisState,
461 => rtsp::StatusCode::UnsupportedTransport,
_ => rtsp::StatusCode::InternalServerError,
}
}
fn is_valid_rtsp_path(uri: &str, configured_path: &str) -> bool {
fn is_valid_rtsp_path(method: &rtsp::Method, uri: &str, configured_path: &str) -> bool {
if matches!(method, rtsp::Method::Options) && uri.trim() == "*" {
return true;
}
let normalized_cfg = configured_path.trim_matches('/');
if normalized_cfg.is_empty() {
return false;
}
let request_path = extract_rtsp_path(uri);
request_path == normalized_cfg
if request_path == normalized_cfg {
return true;
}
if !matches!(method, rtsp::Method::Setup | rtsp::Method::Teardown) {
return false;
}
let control_track_path = format!("{}/trackID=0", normalized_cfg);
request_path == "trackID=0" || request_path == control_track_path
}
fn extract_rtsp_path(uri: &str) -> String {
@@ -1074,8 +1109,13 @@ fn extract_rtsp_path(uri: &str) -> String {
fn is_frame_codec_match(frame: &EncodedVideoFrame, codec: &RtspCodec) -> bool {
matches!(
(frame.codec, codec),
(crate::video::encoder::registry::VideoEncoderType::H264, RtspCodec::H264)
| (crate::video::encoder::registry::VideoEncoderType::H265, RtspCodec::H265)
(
crate::video::encoder::registry::VideoEncoderType::H264,
RtspCodec::H264
) | (
crate::video::encoder::registry::VideoEncoderType::H265,
RtspCodec::H265
)
)
}
@@ -1092,7 +1132,6 @@ fn generate_session_id() -> String {
format!("{:016x}", value)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1109,9 +1148,14 @@ mod tests {
}
}
async fn read_response_from_duplex(mut client: tokio::io::DuplexStream) -> rtsp::Response<Vec<u8>> {
async fn read_response_from_duplex(
mut client: tokio::io::DuplexStream,
) -> rtsp::Response<Vec<u8>> {
let mut buf = vec![0u8; 4096];
let n = client.read(&mut buf).await.expect("failed to read rtsp response");
let n = client
.read(&mut buf)
.await
.expect("failed to read rtsp response");
assert!(n > 0);
let (message, consumed): (rtsp::Message<Vec<u8>>, usize) =
rtsp::Message::parse(&buf[..n]).expect("failed to parse rtsp response");
@@ -1188,13 +1232,57 @@ mod tests {
assert!(fmtp_value.contains("sprop-parameter-sets="));
}
#[test]
fn rtsp_path_matching_follows_sdp_control_rules() {
assert!(is_valid_rtsp_path(
&rtsp::Method::Describe,
"rtsp://127.0.0.1/live",
"live"
));
assert!(is_valid_rtsp_path(
&rtsp::Method::Describe,
"rtsp://127.0.0.1/live/?token=1",
"/live/"
));
assert!(!is_valid_rtsp_path(
&rtsp::Method::Describe,
"rtsp://127.0.0.1/live2",
"live"
));
assert!(!is_valid_rtsp_path(
&rtsp::Method::Describe,
"rtsp://127.0.0.1/",
"/"
));
assert!(is_valid_rtsp_path(
&rtsp::Method::Setup,
"rtsp://127.0.0.1/live/trackID=0",
"live"
));
assert!(is_valid_rtsp_path(
&rtsp::Method::Setup,
"rtsp://127.0.0.1/trackID=0",
"live"
));
assert!(!is_valid_rtsp_path(
&rtsp::Method::Describe,
"rtsp://127.0.0.1/live/trackID=0",
"live"
));
assert!(is_valid_rtsp_path(&rtsp::Method::Options, "*", "live"));
}
#[test]
fn rtsp_path_matching_is_exact_after_normalization() {
assert!(is_valid_rtsp_path("rtsp://127.0.0.1/live", "live"));
assert!(is_valid_rtsp_path("rtsp://127.0.0.1/live/?token=1", "/live/"));
assert!(!is_valid_rtsp_path("rtsp://127.0.0.1/live2", "live"));
assert!(!is_valid_rtsp_path("rtsp://127.0.0.1/", "/"));
fn transport_parsing_detects_tcp_interleaved_requests() {
assert!(is_tcp_transport_request(
"RTP/AVP/TCP;unicast;interleaved=0-1"
));
assert!(is_tcp_transport_request("RTP/AVP;unicast;interleaved=2-3"));
assert!(!is_tcp_transport_request(
"RTP/AVP;unicast;client_port=8000-8001"
));
}
#[test]

View File

@@ -27,6 +27,8 @@ use tracing::{debug, error, info, trace, warn};
/// Grace period before auto-stopping pipeline when no subscribers (in seconds)
const AUTO_STOP_GRACE_PERIOD_SECS: u64 = 3;
/// Restart capture stream after this many consecutive timeouts.
const CAPTURE_TIMEOUT_RESTART_THRESHOLD: u32 = 5;
/// Minimum valid frame size for capture
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
/// Validate JPEG header every N frames to reduce overhead
@@ -1319,6 +1321,7 @@ impl SharedVideoPipeline {
let grace_period = Duration::from_secs(AUTO_STOP_GRACE_PERIOD_SECS);
let mut sequence: u64 = 0;
let mut validate_counter: u64 = 0;
let mut consecutive_timeouts: u32 = 0;
let capture_error_throttler = LogThrottler::with_secs(5);
let mut suppressed_capture_errors: HashMap<String, u64> = HashMap::new();
@@ -1363,11 +1366,27 @@ impl SharedVideoPipeline {
let mut owned = buffer_pool.take(MIN_CAPTURE_FRAME_SIZE);
let meta = match stream.next_into(&mut owned) {
Ok(meta) => meta,
Ok(meta) => {
consecutive_timeouts = 0;
meta
}
Err(e) => {
if e.kind() == std::io::ErrorKind::TimedOut {
consecutive_timeouts = consecutive_timeouts.saturating_add(1);
warn!("Capture timeout - no signal?");
if consecutive_timeouts >= CAPTURE_TIMEOUT_RESTART_THRESHOLD {
warn!(
"Capture timed out {} consecutive times, restarting video pipeline",
consecutive_timeouts
);
let _ = pipeline.running.send(false);
pipeline.running_flag.store(false, Ordering::Release);
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
break;
}
} else {
consecutive_timeouts = 0;
let key = classify_capture_error(&e);
if capture_error_throttler.should_log(&key) {
let suppressed =

View File

@@ -250,6 +250,33 @@ impl WebRtcStreamer {
}
}
fn should_stop_pipeline(session_count: usize, subscriber_count: usize) -> bool {
session_count == 0 && subscriber_count == 0
}
async fn stop_pipeline_if_idle(&self, reason: &str) {
let session_count = self.sessions.read().await.len();
let pipeline = self.video_pipeline.read().await.clone();
let Some(pipeline) = pipeline else {
return;
};
let subscriber_count = pipeline.subscriber_count();
if Self::should_stop_pipeline(session_count, subscriber_count) {
info!(
"{} stopping video pipeline (sessions={}, subscribers={})",
reason, session_count, subscriber_count
);
pipeline.stop();
} else {
debug!(
"Keeping video pipeline alive (reason={}, sessions={}, subscribers={})",
reason, session_count, subscriber_count
);
}
}
/// Ensure video pipeline is initialized and running
async fn ensure_video_pipeline(self: &Arc<Self>) -> Result<Arc<SharedVideoPipeline>> {
let mut pipeline_guard = self.video_pipeline.write().await;
@@ -740,13 +767,7 @@ impl WebRtcStreamer {
session.close().await?;
}
// Stop pipeline if no more sessions
if self.sessions.read().await.is_empty() {
if let Some(ref pipeline) = *self.video_pipeline.read().await {
info!("No more sessions, stopping video pipeline");
pipeline.stop();
}
}
self.stop_pipeline_if_idle("After close_session").await;
Ok(())
}
@@ -763,11 +784,8 @@ impl WebRtcStreamer {
}
}
// Stop pipeline
drop(sessions);
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.stop();
}
self.stop_pipeline_if_idle("After close_all_sessions").await;
count
}
@@ -826,14 +844,9 @@ impl WebRtcStreamer {
sessions.remove(id);
}
// Stop pipeline if no more sessions
if sessions.is_empty() {
drop(sessions);
if let Some(ref pipeline) = *self.video_pipeline.read().await {
info!("No more sessions after cleanup, stopping video pipeline");
pipeline.stop();
}
}
drop(sessions);
self.stop_pipeline_if_idle("After cleanup_closed_sessions")
.await;
}
}
@@ -990,4 +1003,12 @@ mod tests {
let codecs = streamer.supported_video_codecs();
assert!(codecs.contains(&VideoCodecType::H264));
}
#[test]
fn stop_pipeline_requires_no_sessions_and_no_subscribers() {
assert!(WebRtcStreamer::should_stop_pipeline(0, 0));
assert!(!WebRtcStreamer::should_stop_pipeline(1, 0));
assert!(!WebRtcStreamer::should_stop_pipeline(0, 1));
assert!(!WebRtcStreamer::should_stop_pipeline(2, 3));
}
}